Writing tests for plugin¶
When developing a plugin it is important to write tests. The main concern of running
tests is that the test environment has to be separated from the production environment
and care should be taken to avoid any unwanted change to the user’s database.
You may have noticed that aiida_core
has its own test framework for developments.
While it is possible to use the same framework for the plugins,
it is not ideal as any tests of plugins has to be run with
the verdi devel tests
command-line interface.
Special profiles also have to be set mannually by the user and in automated test environments.
AiiDA ships with tools to simplify tests for plugins.
The recommended way is to use the pytest framework, while the unittest package is also supported.
Internally, test environments are created and managed by the aiida.manage.fixtures.fixture_manager()
defined in aiida.manage.fixtures
.
Using the pytest framework¶
In this section we will introduce using the pytest
framework to write tests for
plugins.
Preparing the fixtures¶
One important concept of the pytest framework is the fixture. A fixture is something that a test requires. It could be a predefined object that the test act on, resources for the tests, or just some code you want to run before the test starts. Please see pytest’s documentation for details, especially if you are new to writing testes.
To utilize the fixture_manager
, we first need to define the actual fixtures:
# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""
For pytest
This file should be put into the root directory of the package to make
the fixtures available to all tests.
"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import
import tempfile
import shutil
import pytest
from aiida.manage.fixtures import fixture_manager
@pytest.fixture(scope='session')
def aiida_profile():
"""setup a test profile for the duration of the tests"""
with fixture_manager() as fixture_mgr:
yield fixture_mgr
@pytest.fixture(scope='function')
def new_database(aiida_profile):
"""Get a the database for the test and clean it up after it finishes"""
aiida_profile.reset_db()
return
@pytest.fixture(scope='function')
def new_workdir():
"""get a new temporary folder to use as the computer's wrkdir"""
dirpath = tempfile.mkdtemp()
yield dirpath
shutil.rmtree(dirpath)
The aiida_profile
fixture initialize the fixture_manager
yields it to the test function.
By using the with clause, we ensure that the test profile to run tests are destroyed in the end.
The scope of this fixture should be session, since there is no need to re-initialize the
test profile mid-way.
The next fixture new_database
request the aiida_profile
fixture and tells the received FixtureManager
instance to reset the database.
By requesting the new_database
fixture, the test function will start with a fresh aiida environment.
The next fixture, new_workdir
, returns an temporary directory for file operations and delete it when the test is finished.
You may also want to define other fixtures such as those setup and return Data
nodes or prepare calculations.
To make these fixtures available to all tests, they can be put into the conftest.py
in root level of the package or tests
sub-packages. The code shown above can be downloaded here
.
See also
More information of conftest.py
can be found here.
Import statements in tests¶
When running test, it is important that you DO NOT explicitly load the aiida database
via load_dbenv()
, which could result in corruption of your database with actual data.
However, many AiiDA modules, such as those in aiida.orm
cannot be loaded without calling load_dbenv()
first.
Modules in your plugin may also import such aiida modules at the top level.
Hence, they can not be imported directly in test modules.
To solve this issue, import should be delayed until the test profile has been loaded.
You can always import these required modules inside the test function.
A better way is to define a fixture as a loader for module imports.
For example, instead of having:
import aiida.orm as orm
at the module level, you can define a fixture:
@pytest.fixture(scope='module')
def orm(aiida_profile):
import aiida.orm as orm
return orm
and simply request this fixture for your test function:
def test_load_dataclass(orm):
"""Test loading a data class defined by the plugin"""
from aiida.plugins import DataFactory
MyData = DataFactory('myplugin.maydata')
We set 'scope='module'
to declare that this is module scope fixture and
avoids repetitively doing the import for each test.
It is also possible to group many imports in a single fixture:
@pytest.fixture(scope='module')
def imps(aiida_profile):
"""Return an class with all imports as its attributes"""
class Imports(object):
import aiida.orm as orm
return Imports
def test_load_dataclass(imps):
"""Test loading a data class defined by the plugin"""
MyData = imps.orm.DataFactory('my_plugin.maydata')
Requesting the aiida_profile
fixture in the imps
fixture guarantees
that the test environment will be loaded before the any import statement are executed.
Running the tests¶
Finally, to run the tests, simply type:
pytest
in your terminal from the code directory. The discovery of the tests will be handled by pytest (file, class and function name should start with the word test)
Note
Your terminal will print something out during the creation of a test profile.
Do not panic, as and the aiida profile and database are completely isolated and
will not affect your .aiida
folder and file repositories.
Internally, at temporary folder is used as the .aiida
folder and the test
database are created using the pgtest package.
See also
Before jumping in and start writing your own tests, please takes a look at the tests provided in the aiida-cutter plugin template.
Using the unittest framework¶
The uniitest
package is included in the python standard library.
It is widely used despite some limitations (it is also used for testing aiida_core
).
We provide a aiida.manage.fixtures.PluginTestCase
to be used for inheritance.
By default, each test method in the test case class runs with a fresh aiida database.
Due to the limitation of uniitest
, sub-clasess of PluginTestCase
has to be run
with the special runner in aiida.manage.fixtures.TestRunner
.
To run the actually tests, you need to prepare a run script in python:
import uniitest
from aiida.manage.fixtures import TestRunner
test = unittest.deaultTestLoader.discover('.')
TestRunner().run(tests)
Save it as run_tests.py
and tests can be discovered and run using:
python run_test.py
Migrating existing AiidaTestCase tests¶
The pytest
framework can also be used to run unittest
tests.
Here, we will explain how to migrate existing tests for the plugins,
written as sub-classes of AiidaTestCase
to work with pytest
.
First, let’s see a typical test class using the unittest
:
from aiida.plugins import DataFactory
# Assuming our new date type has entry point myplugin.complex
ComplexData = DataFactory("myplugin.complex")
class TestComplexData(AiidaTestCase):
def setUp(self):
"""Clean up database for each test"""
self.clean_db()
def store_complex(self, comp_num):
"""Store a complex number, returns pk"""
comdata = ComplexData()
comdata.value = comp_num
return comdata.pk
def test_complex_store(self):
"""Test if the complex numbers can be stored"""
comdata = ComplexData()
comdata.value = 1 + 2j
comdata.store()
def test_complex_retrieve(self):
"""Test if the complex
comp_num = 1 + 2j
pk = self.store_complex(cnum)
comdata = load_node(pk)
self.assertEqual(comdata.value == comp_num)
We can modify this test class using some of the pytest features to allow it to be
run with pytest
directly, as shown below:
# Assuming our new date type has entry point myplugin.complex
import unittest
import pytest
@pytest.fixture(scope='module')
def module_import(aiida_profile, request):
from aiida.plugins import DataFactory
ComplexData = DataFactory("myplugin.complex")
for name, value in locals():
setattr(resquest.module, name, value)
@pytest.mark.usefixtures('module_import')
class TestComplexData(TestCase):
"""Test ComplexData. Compatible with pytest."""
@pytest.fixture(autouse=True)
def reset_db(aiida_profile):
aiida_profile.reset_db()
yield
aiida_profile.reset_db()
def store_complex(self, comp_num):
comdata = ComplexData()
comdata.value = comp_num
return comdata.pk
def test_complex_store(self):
"""Test if the complex numbers can be stored"""
comdata = ComplexData()
comdata.value = 1 + 2j
comdata.store()
def test_complex_retrieve(self):
"""
Test if the complex number stored can be retrieved
"""
comp_num = 1 + 2j
pk = self.store_complex(cnum)
comdata = load_node(pk)
self.assertEqual(comdata.value == comp_num)
To allow pytest to run the tests, we first swap the AiidaTestCase
with the generic
TestCase
. We define a module scope fixture module_import
to import the
required AiiDA modules and make them available in the module namespace.
All previous module levels imports should be encapsulated inside this fixture.
The request is a built-in fixture in pytest to allow introspect of the function
from which the fixture is requested.
Here, we simply add every things in the function scope back into the module of the
class which requested the fixture.
Instead of the setUp
and tearDown
methods,
we define a reset_db
fixture to reset the database for every tests.
The autouse=True
flag tells all test methods inside the class to use it automatically.
When migrating your code to use the pytest, you may define a base class with these modifications and use it as the superclass for other test classes.
See also
More details can be found in the pytest documentation about running unittest
tests.
Note
The modification will break the compatibility of uniitest
and you will not be able
to run with verdi devel tests
interface.
Do not forget to remove redundant entry points in your setup.json.