Pytest is the most popular testing library for Python.
It is not included with the Python standard library so it must be installed with [pip](/Programming_Languages/Python/Concepts/Python_package_management.md). While it does not include a declaration library, it is robust enough to handle most scenarios having a rich and expressive set of constructs and decorators that let you declare what your tests should do, under what conditions they should run, and how they should interact with the rest of your system.
### Using `pytest`
- Pytest will automatically detect test files so long as they are named appropriately. E.g. for a module called `lorem`, it will detzect the unit test files `lorem_test.py` and `test_lorem.py`.
- In order to detect tests it should be run from a directory level above them
### Examples
Here is a basic example of using pytest for a local module callled `palindrome`:
```py
import palindrome
def test_is_palindrome():
assert palindrome.is_palindrome('soros')
assert palindrome.is_palindrome('torot')
assert not palindrome.is_palindrome('chair')
```
## Mocking
`patch()` and `Mock` enable us to mock objects whilst testing (classes, functions, methods and properties belonging). They are used in combination.
The `@patch` decorator temporarily replaces a specified object in your code with a mock object and restores the original object after the test is complete
A Mock object simulates the object it replaces so that the object behaves as expected during testing. For example, if your code calls `some_function.some_method()`, and `some_method` is mocked, calling `some_method` will not execute real logic but will interact with the Mock object instead. Mock objects record details about how they have been used, like what methods have been called, with what arguments, etc., allowing you to make assertions about how they have been used.
> `@patch` and `Mock` work together because a patch is used to replace an object or attribute with a Mock object. `Mock` handles the simulated functionality, and `@patch` designates the real value we are replacing with the mock.
### Example case
I will use the following example from one my projects:
This function: sources a URL from an environment variable, interpolates a query string into the URL (which comes in as a parameter), makes a request to the URL, and returns the response as JSON.
It has some safeguards in place:
- It checks that the environment variable is set
- It checks that the request was successful
In the example we could use a Mock object to simulate the response from the Pocket API. This would allow us to test the function without having to make a real request to the API:
```py
def test_successful_request():
# Replace the requests.get function with a Mock object (mock_get)
with patch("requests.get") as mock_get:
# Specify the return value of the mock_get object)
When we mock a function with `patch`, we are replacing the function with a Mock object. This means that the function will not be called. If we want to test the function itself, we can wrap the function in another function and mock the wrapper function instead.
This is useful when you want to track or intercept calls without completely stubbing out or replacing the behavior of the function or method. This can be helpful when you to spy on the internal processes of the given function you are mocking - for instance, ensuring that it calls other functions.
Testing exceptions is quite straightforward. You can use the `raises` helper provided by pytest, and combine this with `excinfo` ("exception info") to inspect the exception message.
```py
if POCKET_LAMBDA_ENDPOINT is None:
raise ValueError(
"Error: POCKET_LAMBDA_ENDPOINT environment variable is not set"
)
```
Then to test this, we would use pytest's `excinfo` fixture along with `raises`:
```py
with pytest.raises(ValueError) as excinfo: # Watch for the ValueError
get_articles("some_type")
assert "Error: POCKET_LAMBDA_ENDPOINT environment variable is not set" in str(
We could actually simplify the above test by using the `match` parameter with `raise`. This way we do not need the separate assertion:
```py
with pytest.raises(ValueError, match="Error: POCKET_LAMBDA_ENDPOINT environment variable is not set"):
get_articles("some_type")
```
Note that `excinfo` is best used for testing the exception text that you the developer explicitly `raise`. For exceptions tha may occur naturaly in the code you are testing, you should use `caplog` or `capsys` (see below).
When testing functions, we achieve this in Python using `setup_function` and `teardown_function` methods. These methods are called before and after each test method respectively.
To apply a "before each" to _every test_ just put your setup function and/or teardown function at the top level of your test module.
For example, below we set and remove an env var before and after each test:
```py
@pytest.fixture(scope="function") # specify that this fixture should be run before each function test
For a sequence of tests that are repetitive, to avoid repeating the same code over and over again, we can use parameterized tests. This is where we pass in a list of parameters to the test function and the test function is run once for each parameter.
For example, in the function below I am handling numerous possible Exceptions that could be raised by the `requests.get` method:
`caplog` and `capsys` are built-in pytest fixtures. `caplog` lets you test log messages. `capsys` lets you test stdout and stderr. As such they are very useful when testing that error messages are logged correctly.
### `caplog`
In our example, if the endpoing environment is not set, we log an error message. We can test that this message is logged correctly using `caplog`:
```py
def test_no_endpoint_env_var(caplog):
os.environ.pop("POCKET_LAMBDA_ENDPOINT", None) # Remove env variable if it exists
with caplog.at_level(logging.ERROR):
result = get_articles("some_type")
assert (
"Error: POCKET_LAMBDA_ENDPOINT environment variable is not set" in caplog.text
)
assert result is None
```
> Note tha we pass in `caplog` as a parameter to the test function. This is how pytest knows to use it as a fixture.
### `capsys`
In our example, if the request is unsuccessful, we log an error message with `print` rather than `logging`. We can test that this message is printed correctly using `capsys` to check the stdout: