|
1 | 1 | # Example Custom NETMIKO CLI test
|
2 | 2 |
|
| 3 | +This repository demonstrates how to create a custom test for NUTS (Network Unit Testing System). After installing this Python package, you can run the following example test: |
| 4 | + |
| 5 | +```yaml |
| 6 | +- test_class: TestNetmikoCLI |
| 7 | + test_module: example_custom_netmiko_cli_test.netmiko_cli |
| 8 | + test_execution: |
| 9 | + command_string: show call-home |
| 10 | + use_timing: False # Determines whether to use send_command_timing (True) or send_command (False) for command execution |
| 11 | + # test_execution parameters are passed to the send_command or send_command_timing function |
| 12 | + # See Netmiko documentation for details: https://ktbyers.github.io/netmiko/docs/netmiko/index.html#netmiko.BaseConnection.send_command |
| 13 | + test_data: |
| 14 | + - host: switch01 |
| 15 | + contains: "call home feature : disable" |
| 16 | + not_contains: "enable" |
| 17 | +``` |
3 | 18 |
|
4 | 19 | ## Setup Project
|
5 | 20 |
|
6 |
| -Example using `uv`: |
| 21 | +This project uses [UV](https://docs.astral.sh/uv/), a fast Python package manager, though you may also use (Poetry)[https://python-poetry.org/] or any other package manager. |
| 22 | +
|
| 23 | +To set up with `uv`, run: |
7 | 24 | ```bash
|
8 | 25 | uv init example-custom-netmiko-cli-test --lib --package -p python3.10
|
9 | 26 | uv add --dev ruff
|
10 | 27 | uv add --dev mypy
|
11 | 28 | uv add nuts
|
12 | 29 | ```
|
13 | 30 |
|
| 31 | +## Test Implementation |
| 32 | + |
| 33 | +The NUTS test class is implemented in `src/example_custom_netmiko_cli_test/netmiko_cli.py`. For detailed instructions on writing custom tests, see the [How To Write Your Own Test](https://nuts.readthedocs.io/en/latest/dev/writetests.html) documentation. |
| 34 | + |
| 35 | +A NUTS test requires three classes: |
| 36 | + |
| 37 | +1. **Context Class**: The `CLIContext` class provides all necessary information for test execution. `CLIContext` inherits from `NornirNutsContext`, which handles the Nornir task execution. |
| 38 | + |
| 39 | +2. **Extractor Class:** The `CLIExtractor` class is responsible for extracting and transforming the task results. When using Nornir, all task results are returned as a `AggregatedResult` object. The extractor processes these results for each host, generating a `NutsResult` object for each, which is then passed to the test class. |
| 40 | + |
| 41 | +3. **Test Class**: `TestNetmikoCLI` is the actual test class, where multiple test functions can be defined for different assertions. |
| 42 | + |
| 43 | + |
| 44 | +### CLIContext |
| 45 | + |
| 46 | +The CLIContext class overrides two methods: |
| 47 | + |
| 48 | +- **nuts_task**: Defines the Nornir task to execute (in this case, `netmiko_send_command`). By default, all `test_execution` parameters are passed as arguments to the task. This behavior can be customized by overriding the `nuts_arguments` method. |
| 49 | + |
| 50 | + ```python |
| 51 | + def nuts_task(self) -> Callable[..., Result]: |
| 52 | + return netmiko_send_command |
| 53 | + ``` |
| 54 | + |
| 55 | +- **nuts_extractor**: Specifies the CLIExtractor object to use for processing results. |
| 56 | + |
| 57 | + ```python |
| 58 | + def nuts_extractor(self) -> CLIExtractor: |
| 59 | + return CLIExtractor(self) |
| 60 | + ``` |
| 61 | + |
| 62 | +To ensure the correct context class is used, set the variable CONTEXT in your file: |
14 | 63 |
|
| 64 | +```python |
| 65 | +CONTEXT = CLIContext |
| 66 | +``` |
| 67 | + |
| 68 | +NUTS will automatically discover and use this context. |
| 69 | + |
| 70 | + |
| 71 | +### CLIExtractor |
| 72 | + |
| 73 | +The `CLIExtractor` prepares the data before passing it to the test class as a `NutsResult` object. By inheriting from `AbstractHostResultExtractor`, it maps the Nornir `AggregatedResult` to each host. The `single_transform` method, called for each host, transforms the `MultiResult` into a `NutsResult`. The `_simple_extract(single_result)` method extracts the first result from the `MultiResult` — standard behavior when there are no Nornir subtasks. |
| 74 | + |
| 75 | +```python |
| 76 | +class CLIExtractor(AbstractHostResultExtractor): |
| 77 | + def single_transform(self, single_result: MultiResult) -> Dict[str, Dict[str, Any]]: |
| 78 | + cli_result = self._simple_extract(single_result) |
| 79 | + return cli_result |
| 80 | +``` |
| 81 | + |
| 82 | +### TestNetmikoCLI |
| 83 | + |
| 84 | +The [custom pytest marker](https://docs.pytest.org/en/stable/example/markers.html) is used to pass specific data from the `test_data` section in the YAML test definition as a pytest fixture. In this example, the `contains` value is passed to the test function, allowing it to validate whether this value appears in the command output. For instance, based on the example YAML, the string `"call home feature : disable"` is passed for `switch01`, and the test checks if it is part of the result. |
| 85 | + |
| 86 | +The test context is accessible via the `nuts_ctx` fixture, which allows additional functionality, such as enhancing error messages if assertions fail. In this example, the code retrieves the command string (`command_string`) for better error reporting. If a value specified in pytest nuts marker is not defined in the test_data section, the test is automatically skipped. |
| 87 | + |
| 88 | +```python |
| 89 | +class TestNetmikoCLI: |
| 90 | + @pytest.mark.nuts("contains") |
| 91 | + def test_contains_in_result( |
| 92 | + self, nuts_ctx, single_result: NutsResult, contains: Any |
| 93 | + ) -> None: |
| 94 | + cmd = nuts_ctx.nuts_parameters.get("test_execution", {}).get( |
| 95 | + "command_string", None |
| 96 | + ) |
| 97 | + result = single_result.result |
| 98 | + assert contains in result, f"'{contains}' NOT found in '{cmd}' output" |
| 99 | +``` |
| 100 | + |
| 101 | +You can also pass multiple values to a test function, as shown in this example from the [documentation](https://nuts.readthedocs.io/en/latest/dev/writetests.html#writing-the-test-itself): |
| 102 | + |
| 103 | +```python |
| 104 | +@pytest.mark.nuts("name, role") |
| 105 | +def test_role(self, single_result: NutsResult, name: str, role: str) -> None: |
| 106 | + assert single_result.result[name]["role"] == role |
| 107 | +``` |
0 commit comments