Repeat Yourself (In Tests)
TLDR: Don't deduplicate code in tests if it hinders readability of a test scenario.
You may be tempted to deduplicate code in your tests, but think carefully before doing so. Will this refactoring increase the clarity of the tests or reduce it? If it won't increase clarity, sticking with duplication might be best for now.
Consider these tests:
@responses.activate
def test_fetches_account(service):
responses.add(
"GET",
"https://api.example.com/api/account/",
json={
"id": "8be4df61-93ca-11d2-aa0d-00e098032b8c",
"name": "John Doe",
"is_active": True,
},
)
account = service.get_account()
assert account.id == UUID("8be4df61-93ca-11d2-aa0d-00e098032b8c")
assert account.name == "John Doe"
assert account.is_active is True
assert account.address is None
assert account.zip_code is None
@responses.activate
def test_fetches_account_with_all_fields(service):
responses.add(
"GET",
"https://api.example.com/api/account/",
json={
"id": "8be4df61-93ca-11d2-aa0d-00e098032b8c",
"name": "John Doe",
"is_active": True,
"address": "Example City, Test St. 123",
"zip_code": "111222",
},
)
account = service.get_account()
assert account.id == UUID("8be4df61-93ca-11d2-aa0d-00e098032b8c")
assert account.name == "John Doe"
assert account.is_active is True
assert account.address == "Example City, Test St. 123"
assert account.zip_code == "111222"A lot of duplicated code. If we give in to the urge to refactor, we might end up with something like this:
account_url = "https://api.example.com/api/account/"
def _set_up_account_response(json):
responses.add("GET", account_url, json=json)
_account_data = {
"id": "8be4df61-93ca-11d2-aa0d-00e098032b8c",
"name": "John Doe",
"is_active": True,
}
_account_data_with_all_fields = {
**_account_data,
"address": "Example City, Test St. 123",
"zip_code": "111222",
}
def _assert_account_fields(account, data):
assert account.id == UUID(data["id"])
assert account.name == data["name"]
assert account.is_active == data["is_active"]
assert account.address == data.get("address")
assert account.zip_code == data.get("zip_code")
@responses.activate
def test_fetches_account(service):
_set_up_account_response(_account_data)
account = service.get_account()
_assert_account_fields(account, _account_data)
@responses.activate
def test_fetches_account_with_all_fields(service):
_set_up_account_response(_account_data_with_all_fields)
account = service.get_account()
_assert_account_fields(account, _account_data_with_all_fields)The test code is much shorter now. But has it become clearer? Absolutely not. You have to jump between four or five definitions to trace exactly what is happening.
A good test tells a story — the situation your code finds itself in and what it's supposed to do.
But what about DRY?
DRY is primarily about having a single source of truth for your business logic, not all code.
I'm not saying you should never deduplicate your test code. If a repeated piece of code doesn't aid readability (specifically, if it doesn't differentiate test scenarios from each other or highlight the connection between the arrange and assert blocks), it may be extracted into a function or a fixture.
See also: