Skip to main content

A modern Python 3 test framework for finding and fixing flaws faster.

Project description

Ward

PyPI version All Contributors

A modern Python test framework designed to help you find and fix flaws faster.

screenshot

Features

This project is a work in progress. Some of the features that are currently available in a basic form are listed below.

  • Descriptive test names: describe what your tests do using strings, not function names.
  • Modular test dependencies: manage test setup/teardown code using fixtures that rely on Python's import system, not name matching.
  • Powerful test selection: limit your test run not only by matching test names/descriptions, but also on the code contained in the body of the test.
  • Colourful, human readable output: quickly pinpoint and fix issues with detailed output for failing tests.
  • Expect API: A simple but powerful assertion API inspired by Jest.
  • Cross platform: Tested on Mac OS, Linux, and Windows.
  • Zero config: Sensible defaults mean running ward with no arguments is enough to get started.

Planned features:

  • Smart test execution order designed to surface failures faster (using various heuristics)
  • Multi-process mode to improve performance
  • Highly configurable output modes
  • Code coverage with --coverage flag
  • Handling flaky tests with test-specific retries, timeouts
  • Integration with unittest.mock (specifics to be ironed out)
  • Plugin system

Getting Started

Install Ward with pip install ward.

Write your first test in test_sum.py (module name must start with "test"):

from ward import expect, test

@test("1 plus 2 equals 3")
def _():
    expect(1 + 2).equals(3)

Now run your test with ward (no arguments needed). Ward will output the following:

 PASS  test_sum: 1 plus 2 equals 3

You've just wrote your first test with Ward, congrats! Look here for more examples.

How to Contribute

Contributions are very welcome and encouraged!

See the contributing guide for information on how you can take part in the development of Ward.

More Examples

Dependency injection with fixtures

In the example below, we define a single fixture named city_list. We can supply this fixture as a default argument to a test, and Ward will resolve it and inject the value into the test. Unlike pytest, Ward doesn't rely on the parameter name matching the name of the fixture, and instead lets you make use of Python's import machinery to specify which fixture you want to inject.

from ward import test, expect, fixture

@fixture
def city_list():
    return ["Glasgow", "Edinburgh"]

@test("'Glasgow' should be contained in the list of cities")
def _(cities=city_list):
    expect("Glasgow").contained_in(cities)

Fixtures can be injected into each other, using the same syntax.

Fixtures are great for extracting common setup code that you'd otherwise need to repeat at the top of your tests, but they can also execute teardown code:

from ward import test, expect, fixture

@fixture
def database():
    db_conn = setup_database()
    yield db_conn
    db_conn.close()


@test(f"Bob is one of the users contained in the database")
def _(db=database):
    # The database connection can be used in this test,
    # and will be closed after the test has completed.
    users = get_all_users(db)
    expect(users).contains("Bob")

The code below the yield statement in the fixture will be executed after the test that depends on it completes, regardless of the result of the test.

By default, a fixture will be executed exactly once each time a test depends on it. This is because the default scope of a fixture is "test".

More specifically, if a test-scoped fixture F is required by multiple other fixtures that are all injected into a single test, then F will only be resolved once.

You can alter the scope of a fixture using the scope argument of the fixture decorator. For example @fixture(scope="module") will tell Ward to only execute the fixture at most once per module, and have all other tests in the module use the cached value from this execution.

Descriptive testing

Test frameworks usually require that you describe how your tests work using a function name. As a result test names are often short and non-descriptive, or long and unreadable.

Ward lets you describe your tests using strings, meaning you can be as descriptive as you'd like:

from ward import expect, test

NAME = "Win Butler"

@test("my_sum(1, 2) is equal to 3")
def _():
    total = my_sum(1, 2)
    expect(total).equals(3)

@test(f"first_char('{NAME}') returns '{NAME[0]}'")
def _():
    first_char = first_char(NAME)
    expect(first_char).equals(NAME[0])

During the test run, Ward will print the descriptive test name to the console:

FAIL  test_things: my_sum(1, 2) is equal to 3
PASS  test_things: first_char('Win Butler') returns 'W'

If you'd still prefer to name your tests using function names, you can do so by starting the name of your test function with test_:

def test_my_sum_returns_the_sum_of_the_input_numbers():
    total = my_sum(1, 2)
    expect(total).equals(3)

Test selection

Search and run matching tests with --search

You can choose to limit which tests are collected and ran by Ward using the --search STRING option. Test names, descriptions and test function bodies will be searched, and those which contain STRING will be ran. Here are some examples:

Run all tests that call the fetch_users function:

ward --search "fetch_users("

Run all tests that check if a ZeroDivisionError is raised:

ward --search "raises(ZeroDivisionError)"

Run all tests decorated with the @xfail decorator:

ward --search "@xfail"

Run a test called test_the_sky_is_blue:

ward --search test_the_sky_is_blue

Run a test described as "my_function should return False":

ward --search "my_function should return False"

Running tests inside a module:

The search takes place on the fully qualified name, so you can run a single module (e.g. my_module) using the following command:

ward --search my_module.

Of course, if a test name or body contains the string "my_module.", that test will also be selected and ran.

This approach is useful for quickly querying tests and running those which match a simple query, making it useful for development.

Specific test selection

Sometimes you want to be very specific when declaring which tests to run.

Ward will provide an option to query tests on name and description using substring or regular expression matching.

(TODO)

The expect API

Use expect to perform tests on objects by chaining together methods. Using expect allows Ward to provide detailed, highly readable output when your tests fail.

from ward import expect, fixture

@fixture
def cities():
    return {"edinburgh": "scotland", "tokyo": "japan", "madrid": "spain"}

def test_capital_cities(cities=cities):
    found_cities = get_capitals_from_server()

    (expect(found_cities)
     .contains("tokyo")                                 # it contains the key 'tokyo'
     .satisfies(lambda x: all(len(k) < 10 for k in x))  # all keys < 10 chars
     .equals(cities))

Most methods on expect have inverted equivalents, e.g. not_equals, not_satisfies, etc.

Working with mocks

expect works well with unittest.mock, by providing methods such as expect.called, expect.called_once_with, and more. If a test fails due to the mock not being used as expected, Ward will print specialised output to aid debugging the problem.

from ward import test, expect
from unittest.mock import Mock

@test("the mock was called with the expected arguments")
def _():
    mock = Mock()
    mock(1, 2, x=3)
    expect(mock).called_once_with(1, 2, x=3)

Checking for exceptions

The test below will pass, because a ZeroDivisionError is raised. If a ZeroDivisionError wasn't raised, the test would fail.

from ward import raises, test

@test("a ZeroDivision error is raised when we divide by 0")
def _():
    with raises(ZeroDivisionError):
        1/0

Running tests in a directory

You can run tests in a specific directory using the --path option. For example, to run all tests inside a directory called tests:

ward --path tests

To run tests in the current directory, you can just type ward, which is functionally equivalent to ward --path .

Skipping a test

Use the @skip annotation to tell Ward not to execute a test.

from ward import skip

@skip
def test_to_be_skipped():
    # ...

You can pass a reason to the skip decorator, and it will be printed next to the test name/description during the run.

@skip("not implemented yet")
@test("everything is okay")
def _():
    # ...

Here's the output Ward will print to the console when it runs the test above:

SKIP  test_things: everything is okay  [not implemented yet]

Expecting a test to fail

You can mark a test that you expect to fail with the @xfail decorator. If a test marked with this decorator passes unexpectedly, the overall run will be considered a failure.

Testing for approximate equality

Check that a value is close to another value.

expect(1.0).approx(1.01, abs_tol=0.2)  # pass
expect(1.0).approx(1.01, abs_tol=0.001)  # fail

Cancelling a run after a specific number of failures

If you wish for Ward to cancel a run immediately after a specific number of failing tests, you can use the --fail-limit option. To have a run end immediately after 5 tests fail:

ward --fail-limit 5

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Darren Burns
Darren Burns

💻 📖 🤔 👀 🐛 💡
khusrokarim
khusrokarim

🤔

This project follows the all-contributors specification. Contributions of any kind welcome!

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ward-0.14.0a0.tar.gz (17.2 kB view hashes)

Uploaded Source

Built Distribution

ward-0.14.0a0-py3-none-any.whl (20.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page