Test API¶
Overview¶
This document describes how to write new factory tests and integrate them into the testing framework.
Basics¶
Factory tests are implemented as Python unit tests. Each factory test
is a subclass of the unittest.TestCase
class, and may make full use of the Python unittest
API (e.g., all
the assert
methods, fail
, setUp
, tearDown
, etc.). The
factory SDK also provides APIs that can be used to parse test
arguments (see Test arguments), and to interact with the test UI
running in the browser (see Test UI API).
Each factory test should at minimum contain
a
runTest
method containing the test implementation (see Test implementation), andan
ARGS
attribute describing the arguments that may be used by the test list when invoking the test (see Test arguments).
Where to put your test¶
Each factory has a name (like bad_blocks
or lcd_backlight
).
For simple tests containing only a Python file, put the test code
in
py/test/pytests/name.py
Alternatively, if your test has accompanying files (such as HTML or JavaScript files), you may create a directory for it and put the code in
py/test/pytests/name/name.py
. *
Within this file, there should be a single subclass of unittest.TestCase. There can be only one test class per module, so if you need to have different test cases, you need to separate them into different modules. But you can create a directory for them, for example:
py/test/pytests/dir_name/test1.py
py/test/pytests/dir_name/test2.py
py/test/pytests/dir_name/test3.py
.
In this case, the name of your tests are dir_name.test1
,
dir_name.test2
and dir_name.test3
.
To know more about how we load a pytest from name, please refer to
cros.factory.test.utils.pytest_utils.LoadPytestModule()
.
- *
Since the
__init__.py
of a package will be loaded whenever its submodule is imported. We recommand keeping__init__.py
empty to prevent longer loading time if__init__.py
is too large.
Test implementation¶
Your test should contain a runTest
method containing the body of
the test. (As with all Python unittests, it may also contain a
setUp
and tearDown
method. setUp
is run first, and
tearDown
is run after the test passes or fails; although note
that tearDown
may not be run if the test is forcibly stopped.)
The test is considered to have succeeded if the runTest
method
returns. The test will fail if:
your test calls any of the
unittest.TestCase.assertXXX
methods, such as assertTrue, and the assertion fails. For example, this will cause your test to fail with a good error message if a file does not exist as expected:- self.assertTrue(
os.path.exists(self.args.path), ‘File %r does not exist’ % self.args.path)
your test calls self.fail <https://docs.python.org/2/library/unittest.html#unittest.TestCase.fail>, which tells the Python unit test framework to raise an exception.
your test directly raises an exception with
raise
.code that you call directly or indirectly raises an exception. For example, if you call
subprocess.check_call
, andcheck_call
raises aCalledProcessError
that you do not catch, your test will fail. If you want such a failure incheck_call
to cause your test to fail, you can choose to not catch the exception and let it propagate out of yourrunTest
method.
There are several key APIs you will need to understand to write your test:
Test arguments allow your test to handle arguments specified in test lists.
Test UI API allows your test to provide a UI to interact with the operator or show status messages. If your test is simple and does not require interaction with the user, you may choose not to provide a UI.
Test arguments¶
Test lists need to customize the behavior of tests in various
situations, e.g., to specify different limits or parameters or to
enable/disable various checks. To allow this sort of customization,
you can declare test arguments in your test case by adding an ARGS
attribute describing the supported set of arguments. ARGS
is a
list of items of type cros.factory.utils.arg_utils.Arg
.
For example:
import unittest
from cros.factory.utils.arg_utils import Arg
class BadBlocksTest(unittest.TestCase):
ARGS = [
Arg('path', str, 'The path to a temporary file to use for testing.'),
Arg('max_bytes', int, 'Maximum size to test, in bytes.',
default=16*1024*1024),
]
This declares two arguments: path
is a required string, and
max_bytes
is an optional number defaulting to 16 megabytes.
The test list might contain an entry like:
{
"pytest_name": "bad_blocks",
"args": {
"path": "/usr/local/foo",
"max_bytes": 8388608
}
}
The factory test runner will check that:
all the arguments in the test list are valid arguments (e.g., you don’t accidentally specify a
filename
argument, sincefilename
is not declared inARGS
).all required arguments (in this case
path
) are specified.all the arguments are of the correct type (e.g., you don’t say
max_bytes='foo'
, sincemax_bytes
must be anint
).
- class cros.factory.utils.arg_utils.Arg(name, type, help, default=<object object>, _transform=None, schema=None)¶
The specification for a single test argument.
- __init__(name, type, help, default=<object object>, _transform=None, schema=None)¶
Constructs a test argument.
- Parameters
name – Name of the argument. This will be the key in a
dargs
dict in the test list.type –
Type of the argument, or (if more than one type is permitted) a tuple of allowable types. If
default
is None, then None is also implicitly allowed. For example:type=int # Allow only integers type=(int, str) # Allow int or string
You can use
enum.Enum
object as a type:class EnumTyped(str, enum.Enum): a = 'a' b = 'b' def __str__(self): return self.name type=EnumTyped # Allows only the members in EnumTyped and str 'a' or 'b'.
Besides, you can use
enum.Enum
functional API:type=enum.Enum('enum_obj', 'a b')
It also supports
enum.IntEnum
object:class IntEnumTyped(enum.IntEnum): num_1 = 1 num_2 = 2 type=IntEnumTyped # Allows only the members in IntEnumTyped and int 1 or 2.
help – A string describing how to use the argument. This will be included in the test catalog in the documentation bundle and may be formatted using reStructuredText.
default – A default value for the argument. If there is no default value, this is omitted.
_transform – A transform function to be applied to the value after the argument is resolved.
schema – A utils.schema object for checking the argument.
Once you have declared the arguments used by your test,
you can use self.args
anywhere in your test implementation
to refer to the value of that argument. For example:
class BadBlocksTest(unittest.TestCase)::
... # see above for ARGS = [...] declaration
def runTest(self):
logging.info('path=%s, max_bytes=%d',
self.args.path, self.args.max_bytes)
- cros.factory.test.utils.pytest_utils.LoadPytestModule(pytest_name)¶
Loads the given pytest module.
This function tries to load the module
cros.factory.test.pytests.pytest_name
.- Parameters
pytest_name – The name of the pytest module.
- Returns
The loaded pytest module object.