assamtest is an experimental python test framework inspired by JavaScript libraries such as mocha or jasmine.
- everything is explicit, no magic
- simple generated tests and suites
- arbitrary nesting of test suites
- compatible with converage
- easily extendable with decorators and custom outcomes
Usage
# tests.py
import assamtest
from assamtest import expect
@assamtest.suite('A suite is just a function')
def my_suite():
@assamtest.test('and so is a test')
def my_test():
a = True
expect.true(a)
$ pip install assamtest
$ assamtest
A suite is just a function
✓ and so is a test
✓ 1 passed
Why another test framework?
The idea for this library came out of my growing frustration with pytest,
especially its parametrize
feature.
In jasmine, parametrization is trivial:
describe('#isNumber', function() {
[1, 1000000, 0, -1].forEach(function(i) {
it('recognizes ' + i, function() {
expect(isNumber(i)).toBe(true);
});
});
});
This is because the tests are registered explicitly. The popular python test frameworks (pytest, unittest) on the other hand use an implicit mechanism where each function that starts with 'test_' is registered. This makes parametrization way harder than it needs to be.
This library is an attempt to bring the explicit approach to python. However, there are two important differences between the languages that make this approach a bit less elegant in python:
-
The test functions will never be called explicitly, so there is really no need for a name. But python does not have anonymous functions. Not a big deal, but still awkward, especially for things like
before_each
andafter_each
. -
In python, variables are local by default. If you want to write to variables from a descendant scope you have to use the
nonlocal
(orglobal
) keyword.
Reference
@test(name=None, args=[], decorators=[])
Register a function as a test:
-
name
(str): The name of this test (defaults to the function name) -
args
(list): Arguments that should be passed to the test function -
decorators
(list): The test function will be passed through these decorators before being executed
Async functions are automatically executed in an event loop.
import assamtest
from assamtest import expect
@assamtest.test(args=['+', 5])
@assamtest.test(args=['*', 6])
def my_test(op, value):
assamtest.expect.equal(eval('2 %s 3' % op), value)
@suite(name=None, args=[], decorators=[])
Register a function as a suite:
import assamtest
from assamtest import expect
@assamtest.suite()
def my_suite():
@assamtest.before_each()
def _before_each():
pass # do some setup here
@assamtest.test()
def my_test():
expect.equal(2 + 2, 4)
The optional parameters are the same as for test()
.
@before()
/ @after()
Register a function to run before/after the whole suite.
There can be only one before
/after
function per suite.
@before_each()
/ @after_each()
Register a function to run before/after every test.
There can be only one before_each
/after_each
function per suite.
expect
A wrapper around the asserts from unittest.TestCase
using snake case:
from assamtest import expect
expect.equal(2 + 2, 4)
expect.not_equal(2 + 2, 5)
expect._in(2, [1, 2, 3])
with expect.raises(KeyError):
{'foo': 0}['bar']
See also the full list of available assertions.
@decorators.skip
Do not execute the test at all::
import assamtest
from assamtest import expect
from assamtest.decorators import skip
@assamtest.test(decorators=[skip])
def my_test():
expect.equal(2 + 2, 5)
@decorators.fail
Invert the result of the test: If it would fail, pass instead. If it would pass, fail instead::
import assamtest
from assamtest import expect
from assamtest.decorators import fail
@assamtest.test(args=[4])
@assamtest.test(args=[5], decorators=[fail])
def my_test(value):
expect.equal(2 + 2, value)
Outcome(err, status, level)
Can be used to implement custom outcomes.
-
err
(Exception|str|None): The reason for this outcome, e.g. an exception or a helpful message -
status
(str): The status, e.g. 'passed', 'failed', or 'skipped' -
level
('SUCCESS'|'INFO'|'WARNING'|'ERROR'): A hint for the reporter how this outcome should be interpreted
A good example of how this can be used is decorators.skip()
:
import functools
from assamtest import Outcome
def skip(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
raise Outcome(None, 'skipped', 'INFO')
return wrapper