Testing¶
Automated tests are key to our success, since they allow us to catch bugs early, run sections of code in isolation, and accelerate our development pace.
Structure¶
Test modules should be located in the same directory as the module they cover. Test modules should be named
test__*.py
(e.g.,test__example.py
). Individual test methods within those modules should be prefixed with
test__
(e.g., test__my_function
).
Test Module Structure
project_root/
├── discord_app_example/
│ ├── __init__.py
│ ├── example.py
│ └── test__example.py
├── conftest.py
└── ...
Running Tests¶
We use the pytest
framework for writing and running our tests. To run the tests,
use the following command from the root of the project:
poetry run pytest
This command will discover and run all the tests modules that match the pattern test__*.py
. You can also run
tests for a specific module by specifying the module name:
poetry run pytest discord_app_example/test__example.py
Running Tests in your IDE
Most modern IDEs have built-in support for running tests. You can run tests directly from your IDE, which can be more convenient than running them from the command line.
The development container is pre-configured for using pytest
in Visual Studio Code.
What to Test¶
Unit tests should cover the following aspects of your code:
- Input validation
- Correctness of output (or outcome) given a valid input
- Error handling
Consider Edge Cases
When writing tests, consider edge cases such as invalid inputs and unexpected behavior. These are often the areas where bugs are most likely to occur.
Some parts of the code may be more critical than others. Focus on writing tests for the most critical parts of the codebase, such as complex algorithms, core functionality or user-facing features.
Writing Tests¶
Each test case should be self-contained and independent of other tests. This means that each test should set up its own data and clean up after itself. Avoid relying on the state of other tests or the order in which tests are run.
When writing tests, follow these guidelines:
- Use descriptive test names that clearly indicate what is being tested.
- Limit each test to a single logical concept.
- Use the
assert
statement to check the expected outcome of the test. - Aim for one
assert
statement per test. - Use fixtures to set up common data or resources.
Example Tests
The examples
folder includes sample tests that you can use as a base for your own test.
Global Fixtures
The conftest.py
file at the root of the project can be used to define fixtures for use across all test modules.
Unit Testing and Type Annotations¶
You can reduce the need for unit tests by indicating the expected types of input arguments and return values as type annotations. While they don't replace unit tests, type annotations can reduce the number of tests you might need to write, particularly those related to input validation.
For instance, consider the following function without type annotations:
Function Without Type Annotations
def add(a, b):
return a + b
Without type annotations, you might write multiple tests to ensure that the function behaves correctly with different types of input, like strings, integers, or floats. But with type annotations:
Function With Type Annotations
def add(a: int, b: int) -> int:
return a + b
The function's expected behavior is clearer. You know that both a
and b
should be integers, and the return
value will also be an integer. With these type annotations in place, there's less need to write unit tests checking
for behaviors with non-integer inputs since the static type checker can catch those mistakes for you.