Yes, there are a host of techniques to deal with code duplication in tests, especially in setup. Which one(s) you use, especially if you're new to testing, depends on things like how familiar you are with the techniques and how much complexity they end up saving you in your particular application.
Probably the simplest method is to just pull out methods to do common setup. You might start with a method to create a user in order to test logging in with a correct and an incorrect password. Once you have that covered you might want to just abstract away both of those with a method to create a user and log in with the correct question, to use as setup for any tests which require you to be logged in. You may end up with something looking like this:
def login(user: User, password: str):
…
def create_user(username: str, password: str) -> User:
…
def ensure_logged_in() -> User:
user = create_user(…)
login(…)
Another way is to "package" related setup and teardown in a TestCase
hierarchy, where none of the *TestCase
classes have tests of their own (otherwise those tests would be repeated for each subclass). These classes usually have just setUpClass
, setUp
, tearDown
and tearDownClass
methods. This way of packaging functionality is really handy since it combines all the setup and teardown in easily reusable classes. By the way, your use of setUp
and tearDown
as class methods is very unorthodox, and I don't think they will work as you expect. You may have classes like
UserTestCase
LoggedInUserTestCase
LoggedInUserWithWritePermissionTestCase
A straightforward TestCase
class hierarchy isn't always optimal. One thing you want to avoid is to create the testing "god class" containing all the setup anyone could ever want, and then cherry-picking parts of that setup during tests. This is a nightmare to maintain for several reasons, the biggest one being the complicated interdependencies between the tests, which make test failures hard to debug and the test code resistant to change. They will also run far slower than they would with minimal setup. One answer to this is to create "mixin" classes containing specific bits of setup and teardown. Your test classes can then inherit from exactly the ones of these they need. For example, a test may inherit from LoggedInAdministratorMixin
and LoggedInRegularUserMixin
to test interactions between these two types of users.
You can also reuse whole tests. To test a bunch of separate API permissions, for example, you can create a class with some tests and then subclass that with just some test setup (a legitimate use for inheriting from a class containing tests, unlike the above). A Stack Overflow answer suggested something close to what I ended up using recently (very simplified, except for the *Tests
classes themselves, which literally ended up just declaring them as test classes):
class RequestPermissionMixin(TestCase):
__test__ = False # Never run this class as a test on its own
request_factory = APIRequestFactory()
def test_should_allow_user_with_permission(self) -> None:
…
def test_should_not_allow_user_without_permission(self) -> None:
…
class CreateMixin(RequestPermissionMixin):
request_factory_method = RequestPermissionMixin.request_factory.post
class UpdateMixin(RequestPermissionMixin):
request_factory_method = RequestPermissionMixin.request_factory.post
class CanCreateFooTests(CreateMixin, CanModifyFooMixin):
__test__ = True
class CanUpdateFooTests(UpdateMixin, CanModifyFooMixin):
__test__ = True