Keep Test Code inside the Test
Noone wants to write one thing twice. Reducing duplicates makes code shorter and clearer. How much this applies for test code?
A few days ago I was pair-programming with another developer in the Test-Driven Development manner. TDD and pair-programming is the best fit. I usually apply TDD after having understood the problem and designed the API, in the implementation phase. This approach helps me reduce the need for later refactoring and avoid testing of trivial code.
Having the requirement reflected in the API, we wrote a test:
public interface Account { boolean canLogin(String password); } @Test void user_can_login_with_a_valid_password() { Account account = new UserAccount("test", "test@example.com", "pwd1"); assertThat(account.canLogin("pwd1")).isTrue(); }
Then we implemented the feature and wrote the next test:
@Test void user_cannot_login_with_an_invalid_password() { Account account = new UserAccount("test", "test@example.com", "pwd1"); assertThat(account.canLogin("xxx")).isFalse(); }
After implementing this we continued:
public interface Account { boolean canLogin(String password); void changePassword(String newPassword); } @Test void password_is_changed() { Account account = new UserAccount("test", "test@example.com", "pwd1"); account.changePassword("updated"); assertThat(account.canLogin("updated")).isTrue(); }
At that moment my partner proposed to move the first line of the tests into a separate method, like this:
private Account account; @BeforeEach void setupAccount() { this.account = new UserAccount("test", "test@example.com", "pwd1"); }
It does follow the Don't Repeat Yourself principle and makes the test methods shorter, right? Look:
@Test void user_can_login_with_a_valid_password() { assertThat(account.canLogin("pwd1")).isTrue(); }
Sounds well first, however, there are several issues.
Don't Share Code among Tests
Sharing code among different requirements is a bad idea, because business is volatile and the rules might change independently. Doing so comes with risks and drawbacks:
1. It's not obvious how the test is set up
Looking at the test only, its not clear what is the arrangement of the test, how is the unit initialized and set up. To understand this, we have to scroll up to the setup method.
2. Tests are coupled
It is impossible to have different setups for different tests, although this is often desired. Consider the following test:
@Test void user_with_no_email_cannot_login() { Account account = new UserAccount("test", null, "pwd1"); assertThat(account.canLogin("pwd1")).isFalse(); }
Such a dilemma often leads to a complex setup method trying to do more than one thing, which makes the test suite difficult to understand and maintain.
3. Tests are not well isolated
Tests must run in isolation from each other. Tests must never be meant to run in a particular order. Consider the following tests:
@Test void user_can_login_with_a_valid_password() { assertThat(account.canLogin("pwd1")).isTrue(); } @Test void password_is_changed() { account.changePassword("updated"); assertThat(account.canLogin("updated")).isTrue(); }
When the second test runs before the first one, it fails.
Even not frequent in practice, it should always be possible to run the tests in parallel. Bearing this in mind helps you to write well isolated tests.
4. Unnecessary work
Consider another test in the test suite:
@Test void empty_password_raises_an_error() { assertThrows(InvalidPasswordException.class, () -> new UserAccount("test", "test@example.com", "")); }
Even not needed at all, the setup method is executed and resources created, which is just a waste.
Don't Share Dependencies amoung Tests
The same is true for dependencies. Instead of doing this (with Spring):
@Autowired private AccountRegistry registry; @Value("${test.admin.username}") private String adminUsername; @Test void registered_user_account_is_in_the_registry() { Account account = new UserAccount("test", "test@example.com", "pwd1", registry); account.register(); assertThat(registry.byUsername("test").isPresent()).isTrue(); } @Test void admin_account_has_admin_rights() { Account account = new UserAccount(adminUsername, "test@example.com", "pwd1"); assertThat(account.hasAdminRights()).isTrue(); }
...it's way better to this (with Spring and JUnit 5):
@Test void registered_user_account_is_in_the_registry(@Autowired AccountRegistry registry) { Account account = new UserAccount("test", "test@example.com", "pwd1", registry); account.register(); assertThat(registry.byUsername("test").isPresent()).isTrue(); } @Test void admin_account_has_admin_rights(@Value("${test.admin.username}") String username) { Account account = new UserAccount(username, "test@example.com", "pwd1"); assertThat(account.hasAdminRights()).isTrue(); }
Conclusion
Write your tests so that everything the test needs is included inside the test. Letting the test code go out of its boundaries makes the test dependent on its context, which brings unnecessary complexity into the test suite.
Benefits of reducing code duplicity don't really count here, because having hard-to-maintain tests pays a much higher price than a few extra lines of code.
Happy testing!