In an ongoing effort to create a GitHub repository / Vscode project that provides an example of a solid development environment for Blender add-ons, I would like to highlight how I set up automated testing.
Blender as a module
We are talking automated unit tests here, and although in principle it would be possible to run a script with the --python option, that wouldn´t provide us with easy test discovery, coverage metrics and easy integration with continuous integration pipelines (like GitHub actions).
Fortunately, Blender can be build as a module and that module is provided as a package on PyPi. This allows us to import the bpy module (and other Blender modules like mathutils and bmesh) like an add-on would inside Blender, yet still run code as a stand-alone Python script. An extremely simplified example is provided in the repo. This example creates an add-on, and, when run stand-alone, also executed that add-on. That works because importing the bpy module also creates an environment just like when opening Blender, so you can register add-ons, execute them or manipulate any kind of Blender data. The only thing that is missing is the UI.
Testing & coverage
Now that we can run scripts that execute add-ons, we can also create automated tests, and with the right setup, they can be automatically discovered and even be run and inspected inside Vscode.
For this I added pytest and pytest-cov to the requirements and configured my Vscode workspace to enable testing (which is pretty simple, just follow the prompts in the Testing panel. More can be found here.)
The resulting settings.json looks like this:
{ "python.testing.pytestArgs": [ "tests", "--cov=add_ons", "--cov-report=xml", "--benchmark-autosave", "--benchmark-skip" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true }
All tests are in a separate directory tests and we will record the coverage just for the add_ons directory (so we do not count the test code itself). Ignore the benchmark options for now, that's for another article perhaps. Any tests that are discovered will then show up the Test panel:
And if you run the tests with coverage reporting, you get an overview to show the coverage (where you can click the individual lines to go to the actual source file and see which lines were covered in the tests)
Anatomy of a Blender unit test
Lets have a look at an example test file to see how we can test our example add-on
We import pytest, the bpy module and the add-on we would like to test:
import pytest import bpy import add_ons.example_simple
and then we create a class that contains all our individual unit tests as methods. The class name starts with Test, that way pytest will automatically discover it and will execute the tests in the methods with names that start with test_
class TestExampleSimple: @classmethod def setup_class(cls): # Ensure the operator is registered before tests if not hasattr(bpy.types, add_ons.example_simple.OPERATOR_NAME): add_ons.example_simple.register() @classmethod def teardown_class(cls): # Unregister the operator after tests if hasattr(bpy.types, add_ons.example_simple.OPERATOR_NAME): add_ons.example_simple.unregister() def test_move_x_operator(self, monkeypatch): # Create a new object and set as active bpy.ops.mesh.primitive_cube_add() obj = bpy.context.active_object obj.location.x = 0.0 # Set the operator amount amount = 2.5 # Call the operator result = bpy.ops.object.move_x("INVOKE_DEFAULT", amount=amount) # Check result and new location assert result == {"FINISHED"} assert pytest.approx(obj.location.x) == amount
The important thing here is to have class methods called setup_class and teardown_class that register and unregister our add-on respectively. Before we register we check if the add-on is already registered and only install if it is not (line 5). We do this because there could be other classes defined in this module that all operate in the same environment that is created when we import bpy, and we don´t know what those other tests might be doing so we play it safe.
An actual test, like test_move_x_operator, is a regular pytest unit test, but we must remember that all those test will operate in the same environment setup by import bpy, so either any actions should be harmless to other test, or they should cleanup after them. Here we add a Cube in object mode, and just leave this lying around after we finish our tests, but for more complex add-ons/operators it would make more sense to remove this Cube again or even reset to the initial state with bpy.ops.wm.read_factory_settings(use_empty=True).
Summary
We saw how to set up an automated testing environment for Blender add-ons using a GitHub repository and VSCode. By installing Blender as a Python module from PyPi, we can import
bpy
and related modules to simulate the Blender environment outside of its UI, enabling the use of tools like pytest
and pytest-cov
for test discovery, coverage reporting, and integration with CI pipelines. With a simple configuration in VSCode, tests can be easily run and inspected, with coverage results clearly visualized. We also explored how to structure test files using setup_class
and teardown_class
methods to safely register and unregister add-ons, and how to write unit tests that interact with Blender data while maintaining a clean and stable testing environment.
No comments:
Post a Comment