Writing tests - sympy/sympy GitHub Wiki
Every new feature in SymPy should have a test associated with it. In addition, whenever a bug is fixed, a test should be added that would fail before the bug was fixed but now passes. That way, the bug cannot be reintroduced without failing the test.
Tests go in the corresponding test file for a given file. For example, for sympy/solvers/ode/ode.py, the tests are in sympy/solvers/ode/tests/test_ode.py. Note that there isn't an exact correspondence between tests and files in all cases. For example, there might be a test file for testing a specific thing that doesn't correspond to a specific submodule. For example, sympy/core/tests/test_args.py tests the entire library and doesn't correspond to any one file.
Test files should have test functions with assertions. A test function should start with test_
and be named something useful corresponding to what is being tested. The test_
prefix is important. Any function that does not have it will not be run by the test runner. The assertions are lines like assert function(x) == result
where function
is what is being tested. For example, here is a test in sympy/functions/elementary/tests/test_exponentials.py
:
def test_exp_infinity():
assert exp(I*y) != nan
assert refine(exp(I*oo)) is nan
assert refine(exp(-I*oo)) is nan
assert exp(y*I*oo) != nan
assert exp(zoo) is nan
x = Symbol('x', extended_real=True, finite=False)
assert exp(x).is_complex is None
Note that the variable y
is defined at the top of the file with no assumptions with
from sympy.abc import x, y, z
but the variable x
needs special assumptions for the test, so it is defined only for the one specific test.
When in doubt, look at how existing tests are written.
Doctests
All docstrings should have examples. See https://docs.sympy.org/latest/documentation-style-guide.html#examples-section. These examples are tested (called "doctests"), but to be sure, you should not think of these as tests. Rather, they are examples that happen to be tested. This is important, because tests and doctests serve different purposes.
-
Tests serve to make sure the behavior of a function is correct. Examples serve to help users understand what a function does.
-
Tests should test lots of examples. Examples should only show as many examples as are useful to help users.
-
Tests should test all corner cases of a function. Examples should only show corner cases if they are instructive. Often corner cases are confusing to show in examples.
-
Tests count toward test coverage. Doctests do not.
-
It doesn't matter what the code in a test looks like, as long as it tests the function. But a confusing example is harder to understand. Hence, you should avoid "working around" issues in doctests in ways that make the examples harder to read. The
...
feature of doctest can help here. In some cases, it may be prudent to just skip a doctest rather than make it so convoluted that it can't be read (using# doctest: +SKIP
).
Basically, don't think of doctests as tests. They are examples that just happen to be tested.
Tests gotchas
There are some gotchas to be careful of when writing tests. PR reviewers should always be on the lookout for these. It is also prudent to occasionally grep the repo to fix any instances of these that slipped in.
-
Tests without an
assert
. For example,# BAD sin(pi) == 0
instead of
# GOOD assert sin(pi) == 0
These won't actually do anything when they fail. Sometimes, this is done intentionally just to make sure something doesn't raise an exception. In that case, a comment should be added to note this.
-
Tests like
assert expr == expr
don't actually do anything. If you want to test that an expression does not evaluate, use theunchanged
helper. For example, instead of# BAD assert Add(x, y) == Add(x, y)
use
# GOOD from sympy.core.expr import unchanged assert unchanged(Add, x, y)
-
The
raises
context manager should test exactly one exception. If there is more than one exception, it can only test the first one. It is better to useraises
with thelambda
form when possible to avoid this issue. For example, instead of# BAD with raises(ValueError): factorint(x) # This line is not actually tested factorint(1.0)
use
# GOOD with raises(ValueError): factorint(x) with raises(ValueError): factorint(1.0)
or even better
# BETTER raises(ValueError, lambda: factorint(x)) raises(ValueError, lambda: factorint(1.0))
-
Similar to
raises
, thewarns
andwarns_deprecated_sympy()
context managers can only test one thing at a time. Instead of# BAD with warns_deprecated_sympy(): deprecated_function1() deprecated_function2()
use
# GOOD with warns_deprecated_sympy(): deprecated_function1() with warns_deprecated_sympy(): deprecated_function2()