Testing - Nautobot Design Builder Documentation

Skip to content

# Testing

## `nautobot_design_builder.testing`

This module provides a framework for testing designs.

### `BuilderChecks`

Collection of static methods for testing designs.

Source code in `nautobot_design_builder/testing.py`

    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    class BuilderChecks:
        """Collection of static methods for testing designs."""

        @staticmethod
        def check_connected(test, check, index):
            """Check if two endpoints are connected with a cable."""
            value0 = _get_value(check[0])[0]
            value1 = _get_value(check[1])[0]
            cables = Cable.objects.filter(
                Q(termination_a_id=value0.id, termination_b_id=value1.id)
                | Q(termination_a_id=value1.id, termination_b_id=value0.id)
            )
            test.assertEqual(1, cables.count(), msg=f"Check {index}")

        @staticmethod
        def check_equal(test, check, index):
            """Check that two values are equal."""
            value0 = _get_value(check[0])
            value1 = _get_value(check[1])

            # TODO: Mysql tests fail due to unordered lists
            if isinstance(value0, list) and isinstance(value1, list):
                test.assertEqual(len(value0), len(value1))
                for item0 in value0:
                    test.assertIn(item0, value1)
            else:
                test.assertEqual(value0, value1, msg=f"Check {index}")

        @staticmethod
        def check_model_exists(test, check, index):
            """Check that a model exists."""
            values = _get_value(check)
            test.assertEqual(len(values), 1, msg=f"Check {index}")

        @staticmethod
        def check_model_not_exist(test, check, index):
            """Check that a model does not exist."""
            values = _get_value(check)
            test.assertEqual(len(values), 0, msg=f"Check {index}")

        @staticmethod
        def check_in(test, check, index):
            """Check that a model does not exist."""
            value0 = _get_value(check[0])[0]
            value1 = _get_value(check[1])
            if len(value1) == 1:
                value1 = value1[0]
            test.assertIn(value0, value1, msg=f"Check {index}")

        @staticmethod
        def check_not_in(test, check, index):
            """Check that a model does not exist."""
            value0 = _get_value(check[0])[0]
            value1 = _get_value(check[1])
            if len(value1) == 1:
                value1 = value1[0]
            test.assertNotIn(value0, value1, msg=f"Check {index}")

#### `check_connected(test, check, index)` `staticmethod`

Check if two endpoints are connected with a cable.

Source code in `nautobot_design_builder/testing.py`

    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    @staticmethod
    def check_connected(test, check, index):
        """Check if two endpoints are connected with a cable."""
        value0 = _get_value(check[0])[0]
        value1 = _get_value(check[1])[0]
        cables = Cable.objects.filter(
            Q(termination_a_id=value0.id, termination_b_id=value1.id)
            | Q(termination_a_id=value1.id, termination_b_id=value0.id)
        )
        test.assertEqual(1, cables.count(), msg=f"Check {index}")

#### `check_equal(test, check, index)` `staticmethod`

Check that two values are equal.

Source code in `nautobot_design_builder/testing.py`

    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    @staticmethod
    def check_equal(test, check, index):
        """Check that two values are equal."""
        value0 = _get_value(check[0])
        value1 = _get_value(check[1])

        # TODO: Mysql tests fail due to unordered lists
        if isinstance(value0, list) and isinstance(value1, list):
            test.assertEqual(len(value0), len(value1))
            for item0 in value0:
                test.assertIn(item0, value1)
        else:
            test.assertEqual(value0, value1, msg=f"Check {index}")

#### `check_in(test, check, index)` `staticmethod`

Check that a model does not exist.

Source code in `nautobot_design_builder/testing.py`

    61
    62
    63
    64
    65
    66
    67
    68
    @staticmethod
    def check_in(test, check, index):
        """Check that a model does not exist."""
        value0 = _get_value(check[0])[0]
        value1 = _get_value(check[1])
        if len(value1) == 1:
            value1 = value1[0]
        test.assertIn(value0, value1, msg=f"Check {index}")

#### `check_model_exists(test, check, index)` `staticmethod`

Check that a model exists.

Source code in `nautobot_design_builder/testing.py`

    49
    50
    51
    52
    53
    @staticmethod
    def check_model_exists(test, check, index):
        """Check that a model exists."""
        values = _get_value(check)
        test.assertEqual(len(values), 1, msg=f"Check {index}")

#### `check_model_not_exist(test, check, index)` `staticmethod`

Check that a model does not exist.

Source code in `nautobot_design_builder/testing.py`

    55
    56
    57
    58
    59
    @staticmethod
    def check_model_not_exist(test, check, index):
        """Check that a model does not exist."""
        values = _get_value(check)
        test.assertEqual(len(values), 0, msg=f"Check {index}")

#### `check_not_in(test, check, index)` `staticmethod`

Check that a model does not exist.

Source code in `nautobot_design_builder/testing.py`

    70
    71
    72
    73
    74
    75
    76
    77
    @staticmethod
    def check_not_in(test, check, index):
        """Check that a model does not exist."""
        value0 = _get_value(check[0])[0]
        value1 = _get_value(check[1])
        if len(value1) == 1:
            value1 = value1[0]
        test.assertNotIn(value0, value1, msg=f"Check {index}")

### `BuilderTestCase`

Bases: `TestCase`, `RunChecksMixin`

This class provides a framework for running tests locally with defined YAML files.

Source code in `nautobot_design_builder/testing.py`

    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    class BuilderTestCase(TestCase, RunChecksMixin, metaclass=_BuilderTestCaseMeta):
        """This class provides a framework for running tests locally with defined YAML files."""

        def _run_test_case(self, testcase, data_dir):
            with patch("nautobot_design_builder.design.Environment.roll_back") as roll_back:
                self.run_checks(testcase.get("pre_checks", []))

                depends_on = testcase.pop("depends_on", None)
                if depends_on:
                    depends_on_path = os.path.join(data_dir, depends_on)
                    depends_on_dir = os.path.dirname(depends_on_path)
                    with open(depends_on_path, encoding="utf-8") as file:
                        self._run_test_case(yaml.safe_load(file), depends_on_dir)

                extensions = []
                for extension in testcase.get("extensions", []):
                    extensions.append(_load_class(extension))

                with self.captureOnCommitCallbacks(execute=True):
                    for design in testcase["designs"]:
                        environment = Environment(extensions=extensions)
                        commit = design.pop("commit", True)
                        environment.implement_design(design=design, commit=commit)
                        if not commit:
                            roll_back.assert_called()

                self.run_checks(testcase.get("checks", []))

### `DesignTestCase`

Bases: `TestCase`

DesignTestCase aides in creating unit tests for design jobs and templates.

Source code in `nautobot_design_builder/testing.py`

    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    class DesignTestCase(TestCase):
        """DesignTestCase aides in creating unit tests for design jobs and templates."""

        def setUp(self):
            """Setup a mock git repo to watch for config context creation."""
            super().setUp()
            self.data = {
                "deployment_name": "Test Design",
            }
            self.logged_messages = []
            self.git_patcher = patch("nautobot_design_builder.ext.GitRepo")
            self.git_mock = self.git_patcher.start()

            self.git_path = tempfile.mkdtemp()
            git_instance_mock = PropertyMock()
            git_instance_mock.return_value.path = self.git_path
            self.git_mock.side_effect = git_instance_mock

        def get_mocked_job(self, design_class: Type[DesignJob]):
            """Create an instance of design_class and properly mock request and job_result for testing."""
            job_model = Job.objects.get(module_name=design_class.__module__, job_class_name=design_class.__name__)
            job = design_class()
            job.job_result = JobResult.objects.create(
                name="Fake Job Result",
                job_model=job_model,
            )
            job.saved_files = {}

            def save_design_file(filename, content):
                job.saved_files[filename] = content

            job.save_design_file = save_design_file
            self.logged_messages = []

            class _CaptureLogHandler(logging.Handler):
                def emit(handler, record: logging.LogRecord) -> None:  # pylint:disable=no-self-argument,arguments-renamed
                    message = handler.format(record)
                    obj = getattr(record, "object", None)
                    self.logged_messages.append(
                        {
                            "message": message,
                            "obj": obj,
                            "level_choice": record.levelname,
                            "grouping": getattr(record, "grouping", record.funcName),
                        }
                    )

            job.logger.addHandler(_CaptureLogHandler())
            return job

        def assert_context_files_created(self, *filenames):
            """Confirm that the list of filenames were created as part of the design implementation."""
            for filename in filenames:
                self.assertTrue(os.path.exists(os.path.join(self.git_path, filename)), f"{filename} was not created")

        def tearDown(self):
            """Remove temporary files."""
            self.git_patcher.stop()
            shutil.rmtree(self.git_path)
            super().tearDown()

#### `assert_context_files_created(*filenames)`

Confirm that the list of filenames were created as part of the design implementation.

Source code in `nautobot_design_builder/testing.py`

    268
    269
    270
    271
    def assert_context_files_created(self, *filenames):
        """Confirm that the list of filenames were created as part of the design implementation."""
        for filename in filenames:
            self.assertTrue(os.path.exists(os.path.join(self.git_path, filename)), f"{filename} was not created")

#### `get_mocked_job(design_class)`

Create an instance of design_class and properly mock request and job_result for testing.

Source code in `nautobot_design_builder/testing.py`

    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    def get_mocked_job(self, design_class: Type[DesignJob]):
        """Create an instance of design_class and properly mock request and job_result for testing."""
        job_model = Job.objects.get(module_name=design_class.__module__, job_class_name=design_class.__name__)
        job = design_class()
        job.job_result = JobResult.objects.create(
            name="Fake Job Result",
            job_model=job_model,
        )
        job.saved_files = {}

        def save_design_file(filename, content):
            job.saved_files[filename] = content

        job.save_design_file = save_design_file
        self.logged_messages = []

        class _CaptureLogHandler(logging.Handler):
            def emit(handler, record: logging.LogRecord) -> None:  # pylint:disable=no-self-argument,arguments-renamed
                message = handler.format(record)
                obj = getattr(record, "object", None)
                self.logged_messages.append(
                    {
                        "message": message,
                        "obj": obj,
                        "level_choice": record.levelname,
                        "grouping": getattr(record, "grouping", record.funcName),
                    }
                )

        job.logger.addHandler(_CaptureLogHandler())
        return job

#### `setUp()`

Setup a mock git repo to watch for config context creation.

Source code in `nautobot_design_builder/testing.py`

    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    def setUp(self):
        """Setup a mock git repo to watch for config context creation."""
        super().setUp()
        self.data = {
            "deployment_name": "Test Design",
        }
        self.logged_messages = []
        self.git_patcher = patch("nautobot_design_builder.ext.GitRepo")
        self.git_mock = self.git_patcher.start()

        self.git_path = tempfile.mkdtemp()
        git_instance_mock = PropertyMock()
        git_instance_mock.return_value.path = self.git_path
        self.git_mock.side_effect = git_instance_mock

#### `tearDown()`

Remove temporary files.

Source code in `nautobot_design_builder/testing.py`

    273
    274
    275
    276
    277
    def tearDown(self):
        """Remove temporary files."""
        self.git_patcher.stop()
        shutil.rmtree(self.git_path)
        super().tearDown()

### `RunChecksMixin`

Mixin for running checks on a testcase.

Source code in `nautobot_design_builder/testing.py`

    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    class RunChecksMixin:  # pylint:disable=too-few-public-methods
        """Mixin for running checks on a testcase."""

        def run_checks(self, checks):
            """Run checks on a testcase."""
            for index, check in enumerate(checks):
                for check_name, args in check.items():
                    _check_name = f"check_{check_name}"
                    if hasattr(BuilderChecks, _check_name):
                        getattr(BuilderChecks, _check_name)(self, args, index)
                    else:
                        raise ValueError(f"Unknown check {check_name} {check}")

#### `run_checks(checks)`

Run checks on a testcase.

Source code in `nautobot_design_builder/testing.py`

    152
    153
    154
    155
    156
    157
    158
    159
    160
    def run_checks(self, checks):
        """Run checks on a testcase."""
        for index, check in enumerate(checks):
            for check_name, args in check.items():
                _check_name = f"check_{check_name}"
                if hasattr(BuilderChecks, _check_name):
                    getattr(BuilderChecks, _check_name)(self, args, index)
                else:
                    raise ValueError(f"Unknown check {check_name} {check}")

### `VerifyDesignTestCase`

Bases: `DesignTestCase`, `RunChecksMixin`

VerifyDesignTestCase aides in verifying the test cases with queries.

Source code in `nautobot_design_builder/testing.py`

    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    class VerifyDesignTestCase(DesignTestCase, RunChecksMixin):
        """VerifyDesignTestCase aides in verifying the test cases with queries."""

        job_design = None
        check_file = None
        job_data = {}

        def run_design_test(self):
            """This is what class's that inherit from `VerifyDesignTestCase` call to setup and run."""
            if self.job_design is None:
                raise ValueError("The class attribute `job_design` was not defined")
            if self.check_file is None:
                raise ValueError("The class attribute `check_file` was not defined")

            job = self.get_mocked_job(self.job_design)
            job.run(data=self.job_data, dryrun=False)
            with open(self.check_file, "r", encoding="utf-8") as file:
                checks_data = yaml.safe_load(file)
                checks = checks_data.get("checks", [])
                if not checks:
                    raise ValueError(f"Check file {self.check_file} does not contain any checks")
            self.run_checks(checks)

#### `run_design_test()`

This is what class's that inherit from `VerifyDesignTestCase` call to setup and run.

Source code in `nautobot_design_builder/testing.py`

    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    def run_design_test(self):
        """This is what class's that inherit from `VerifyDesignTestCase` call to setup and run."""
        if self.job_design is None:
            raise ValueError("The class attribute `job_design` was not defined")
        if self.check_file is None:
            raise ValueError("The class attribute `check_file` was not defined")

        job = self.get_mocked_job(self.job_design)
        job.run(data=self.job_data, dryrun=False)
        with open(self.check_file, "r", encoding="utf-8") as file:
            checks_data = yaml.safe_load(file)
            checks = checks_data.get("checks", [])
            if not checks:
                raise ValueError(f"Check file {self.check_file} does not contain any checks")
        self.run_checks(checks)

### `attrgetter`

Return a callable object that fetches attr or key from its operand.

The attribute names can also contain dots

Source code in `nautobot_design_builder/testing.py`

     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    class attrgetter:  # pylint:disable=invalid-name,too-few-public-methods
        """Return a callable object that fetches attr or key from its operand.

        The attribute names can also contain dots
        """

        def __init__(self, attr):
            """Initialize the attrgetter object."""
            if not isinstance(attr, str):
                raise TypeError("attribute name must be a string")
            self._attrs = (attr,)
            names = attr.split(".")

            def func(obj):
                for name in names:
                    if hasattr(obj, name):
                        obj = getattr(obj, name)
                    elif name in obj:
                        obj = obj[name]
                    else:
                        raise AttributeError(f"'{type(obj).__name__}' has no attribute or item '{name}'")
                return obj

            self._call = func

        def __call__(self, obj):
            """Call the attrgetter object."""
            return self._call(obj)

#### `__call__(obj)`

Call the attrgetter object.

Source code in `nautobot_design_builder/testing.py`

    105
    106
    107
    def __call__(self, obj):
        """Call the attrgetter object."""
        return self._call(obj)

#### `__init__(attr)`

Initialize the attrgetter object.

Source code in `nautobot_design_builder/testing.py`

     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    def __init__(self, attr):
        """Initialize the attrgetter object."""
        if not isinstance(attr, str):
            raise TypeError("attribute name must be a string")
        self._attrs = (attr,)
        names = attr.split(".")

        def func(obj):
            for name in names:
                if hasattr(obj, name):
                    obj = getattr(obj, name)
                elif name in obj:
                    obj = obj[name]
                else:
                    raise AttributeError(f"'{type(obj).__name__}' has no attribute or item '{name}'")
            return obj

        self._call = func