W

Ward
GitHub logo
216 stars, 18 forks

Pytest comparison

Here are some of the key differences between ward and pytest. This article assumes a working knowledge of pytest concepts.

Fixtures

In pytest, you inject a fixture into a test by defining a parameter of the same name. To share pytest fixtures across files, you can place them in a conftest.py file.

@pytest.fixture
def num():
return 42
def test_number_is_42(num): # param name must match fixture name
assert num == 42

In ward, you inject a fixture by importing it and binding it as a default argument in your test. This means IDE features such as "go to definition" work for fixtures. You don't need to learn rules for fixture resolution or worry that you may override a fixture or inject the wrong one.

@fixture
def num():
yield 42
@test("the number is 42")
def _(n=num):
assert n == 42

Alternatively, you can use the @using decorator to bind a fixture to a positional argument:

@test("the number is 42")
@using(n=num)
def _(n):
assert n == 42

To share ward fixtures across files, you can just import them as you would any other Python function. In both ward and pytest, you can execute teardown code in a fixture by placing it after a yield statement.

Declaring tests

Pytest test names must begin with the word "test". They can be functions or methods in a class.

In ward, tests are indicated using the @test(description) decorator. The function itself can be named _, since the name would likely just repeat the description in a less readable form. Class level tests are not supported.

In both cases, tests are collected from modules starting with "test_" or ending with "_test".

Fixture scopes

Pytest currently supports function, class, module, package and session scopes. As of v5.2, it now supports dynamic scopes.

Ward currently supports Test (which I think is equivalent to pytest function scope), Module and Global (same as pytest session) scopes.

Assertions

Both Ward and Pytest support assertion rewriting, meaning you can use the Python assert statement and still get detailed information on the assertion should it fail.

Pytest rewrites assertions using an import hook, whereas Ward rewrites the bodies of tests that are collected.

Parameterisation

In pytest, you can parameterise tests using the pytest.mark.parametrize decorator:

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected

This test translated into ward would look like:

@test("eval returns expected output")
def _(
test_input=each("3+5", "2+4", "6*9"),
expected=each(8, 6, 42),
):
assert eval(test_input) == expected

There appears to be no simple way to use a fixture when parameterising a test in pytest. In ward, if you refer to a fixture when parameterising, it will be resolved:

@fixture
def forty_two():
yield 42
@test("eval returns expected output")
def _(
test_input=each("3+5", "2+4", "6*9"),
expected=each(8, 6, forty_two),
):
assert eval(test_input) == expected

Pytest also lets you parameterise fixtures themselves. Ward does not support this.

Output

A comparison between the output of Pytest and Ward is shown below.

Pytest was run with the -v flag in order to display the full diffs for failing tests. Unfortunately, when you run pytest with -v, all of the output becomes more verbose. If you want to see a full diff for any tests that fail, you have to accept that pytest will print each test on a separate line during the run:

pytest-output

Ward relies on colours rather than symbols to present diffs:

ward-output

Development

Pytest is a mature production ready testing framework. On the other hand, Ward has not been production tested and is currently in beta.

Extensibility

Pytest has a plugin system powered by pluggy. Ward does not yet have a plugin system.