Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mocking guidance #205

Merged
merged 4 commits into from
Nov 7, 2024
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 58 additions & 5 deletions book/testing_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def test_sum_columns():
assert_frame_equal(expected_output, actual_output)
```

Using minimal and general data in the test has made it clearer what is being tested.
Using minimal and general data in the test has made it clearer what is being tested, and also avoids any unnecessary disclosure.
In this case our function is very generic, so our test doesn't need to know the names of real columns in our data or even have similar values in the data.
The test data are focussed on testing specific, realistic cases.
This makes it easy to see that this function works correctly with positive, negative and zero values.
Expand Down Expand Up @@ -321,12 +321,65 @@ Integration tests that check very specific outcomes will need to be updated with
User acceptance tests are those that check that a high level user requirement has been met.
In analysis, these are likely part of an end to end test that checks that the output is fit for purpose.

```{todo}
Discuss testing interface with external systems (e.g. database).
Test that your code works, given the format of response that the system can give.
Mocks?
## Isolate code tests from external systems

Testing code that interacts with an external system can be particularly challenging when you can't guarantee
that the system will provide you with the same response each time; this could include code querying a database or
making API requests, for example.

Best practice is to seperate external system dependencies from your code tests as much as possible. This can mitigate against various risks, depending on your application:
gisellerosetta marked this conversation as resolved.
Show resolved Hide resolved

- Testing database interaction with a production database could result in damage to, or loss of, data.
- Making real API calls when testing a function that handles requests could incur unintended monetary costs.
gisellerosetta marked this conversation as resolved.
Show resolved Hide resolved

Isolating code from external systems allows for tests to run without reliance on the real systems; for example, tests for a database interaction that can still run even if the database connection goes down.
Testing your code in this way means that tests evaluate how your code handles an output or response, and not the system dependency itself. This has the added benefit of helping you understand when errors are coming from an external system, as opposed to your code.

One way of achieving this is with mocking, where a response from an outside system is replaced with a mock object that your code can be tested against. In this example, there's a function making an API request in `src/handle_api_request.py`, and two test functions in `tests/test_handle_api_request.py`. The response from `requests.get()` is mocked with a `Mock()` object, to which `text` and `status_code` attributes are assigned, so that the `get_response()` function can be evaluated for how it handles successful and unsuccessful requests. Thanks to the mocking, get requests are not made to `http://example.com`. AI has been used to produce content within this artefact.

```{code-block} python
# src/handle_api_request.py
import requests

def get_response(url: str):
response = requests.get(url)
if response.status_code != 200:
raise(requests.HTTPError("Unsuccessful request"))

return response.text

...

# tests/test_handle_api_request.py
from src.api_requests import get_response
import requests
import pytest
from unittest import mock

@mock.patch("requests.get")
def test_get_response_success(mock_requests_get):
mock_response = mock.Mock()
mock_response.text = "Successful"
mock_response.status_code = 200

mock_requests_get.return_value = mock_response

actual = get_response("http://example.com")
assert(actual == "Successful")
gisellerosetta marked this conversation as resolved.
Show resolved Hide resolved

@mock.patch("requests.get")
def test_get_response_fail(mock_requests_get):
mock_response = mock.Mock()
mock_response.status_code = 400
mock_requests_get.return_value = mock_response

with pytest.raises(requests.HTTPError):
actual = get_response("http://example.com")

gisellerosetta marked this conversation as resolved.
Show resolved Hide resolved
```

[Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html#how-to-monkeypatch-mock-modules-and-environments) in `pytest` provides an alterative way of handling mock objects and attributes, and also allows for the mocking of environment variables.
gisellerosetta marked this conversation as resolved.
Show resolved Hide resolved

## Write tests to assure that bugs are fixed

Each time you find a bug in your code, you should write a new test to assert that the code works correctly.
Expand Down
Loading