Python - How To Write Pytest

Pytest Is The New Black (Compared To Unittest)

Posted by Rico's Nerd Cluster on February 1, 2019

Run Pytest

  • Run a specific test file pytest path/to/test_file.py
  • Run a test in it: pytest path/to/test_file.py::test_function_name

Assert

  • For integer assertions:
1
assert (1==1)
  • For float assertions:
1
2
import pytest
1.0==pytest.approx(1.0)
  • For numpy array assertions:
1
2
3
4
import numpy as np
array1 = np.array([1, 2, 3])
array2 = np.array([1, 2, 3])
np.testing.assert_allclose(array1, array2)

Using VSCode Debugger With Pytest

  1. ctrl+shift+p choose debug tests in the current file or debug all tests (if you want to debug all tests under a configured directory)
  2. In my debugger, I found that I have to manually set a breakpoint before the failure point in Pytest. (I might miss an easier route to get around this)
  3. At the failed test, right click, and choose debug test

  4. pytest -s <my_test.py> seems to be executing all test modules in the current directory: this will be enforced in a pyproject.toml environment.

Test Fixture

The boiler plate is:

1
2
3
4
5
6
7
8
9
import pytest
@pytest.fixture
def basic_config():
    return {
        "batch_size": 2,
    }
def test_og_positional_encoder(basic_config):
    batch_size = basic_config["batch_size"]
    ...

Patching

Patching a Function

To patch my_func() from MyModule, pytest requires it to be a MagicMock object. This means we must specify return_value and pass mock_my_func into the test function.

1
2
3
4
5
6
7
8
from unittest.mock import patch
import requests

@patch('MyModule.my_func', return_value=my_func_patch())
def test_get_license_hash(mock_my_func):
    """Test if the server responds with 200 OK."""
    response = requests.get(f"{SERVER_URL}/GetLicenseHash")
    assert response.status_code == 200

Patching a Constant or List

Unlike functions, patching constants or lists does not require a MagicMock. Instead, patch directly replaces the original object with a real one, meaning no return_value or mock object argument is needed.

1
2
3
4
5
@patch('MyModule.WEBCAM_RESTART_COMMAND', ["echo", "hello"])
def test_webcam_restart():
    """Test if the webcam restart command is patched correctly."""
    response = requests.get(f"{SERVER_URL}/RestartService")
    assert response.status_code == 200

Configure Pytests

In conftext.py, you can add a fixture to find float_dtype

1
2
3
@pytest.fixture(scope="session")
def float_dtype(request):
    return torch.float16 if request.config.getoption("--fp16") else torch.float32

Then, pytest will automatically inject this fixture if you test has it:

1
def test_gathering_forward_output_shape(float_dtype):

Pylint

The duplicate-code warning is expected here. Pylint rule R0801 is triggered because several files contain nearly identical ROS 2 boilerplate. For example, the main() stubs in _compressor_node.py and _decompressor_node.py are very similar, and the same is true for the two launch files. You see this pattern across much of the codebase—such as knowledge_base and kb_visual_tools—because ROS 2 node setup naturally repeats. Unless we want to factor that boilerplate into a shared helper module, suppressing this warning is the practical choice.

Pylint’s duplicate-code check is purely textual. It compares all files passed to pylint in a single run and reports when the number of consecutive identical lines between two files exceeds its threshold (four lines by default). It does not consider whether the repeated code is simple, intentional, or idiomatic.

So if two files both contain something like:

```python rclpy.init(args=args) node = None try: node = SomeNode() rclpy.spin(node)