I have a rather complicated Django (Rest Framework) view which updates an object in the database.
In order to update the object, some conditions need to be met (these aren't the real conditions, but they're similar):
admin
is_moon_phase_ok
needs to return True
I'm trying to write a solid set of unit tests for this view and the scenarios that I've come up with are the following:
when not logged in, return 401
when not logged in, return {"fail": "login"}
when not logged in, don't touch database
when not logged in, don't check moon phase
when username is not admin_*, return 401
when username is not admin_*, return {"fail": "username"}
when username is not admin_*, don't touch database
when username is not admin_*, don't check moon phase
when invalid data, return 400
when invalid data, return {"fail": "data"}
when invalid data, don't touch database
when invalid data, don't check moon phase
when logged in, return 200
when logged in, return updated data
when logged in, check moon phase
when logged in, update database
As you can tell, each of these unit tests will take quite a bit of code to set up and then execute. In my case, it's between 7 and 20 lines of code per unit test.
Imagine if the requirements change, and they do change, how much of a pain it would be to overlook the test cases, make sure they still apply, update them according to the new requirements, etc.
Is there a better way of accomplishing the same thing, with the same test coverage, but with less labor?
For me this sounds a lot like parametrized tests, pytest have support for this, basically you can write a test and provide input parameters, as well as expected. So you write just one test but generic enough to support different parameters, thus less code to maintain. Behind the scene pytest runs same test one by one with your defined parameters. Writing generic tests may introduce some logic (as you can see in my example) but you can live with that in my opinion
As an generic example:
@pytest.mark.parametrize('is_admin,expected_status_code,expected_error', [
(True, 200, {}),
(False, 401, {"fail": "login"})
])
def test_sample(is_admin, expected_status_code, expected_data):
# do your setup
if is_admin:
user = create_super_user()
else:
user = normal_user()
# do your request
response = client.get('something')
# make assertion on response
assert response.status_code == expected_status_code
assert response.data == expected_data
You can also have multiple layers of parameters, eg.:
@pytest.mark.parametrize('is_admin', [
True,
False
])
@pytest.mark.parametrize('some_condition,expected_status_code,expected_error', [
(True, 200, {}),
(False, 401, {"fail": "login"})
])
This will execute the tests for each combination of is_admin (True/False) and other parameters, nice huh?
Check documentation here pytest parametrize tests
If you don't use pytest, check this library that does something similar Parameterized testing with any Python test framework