Writing Your F.I.R.S.T Unit Tests
First class developers write their code test-first using FIRST. Tests should be fast, independent, repeatable, self-validating, and timely.
Join the DZone community and get the full member experience.
Join For FreeUnit tests are required to test singular sections of code. In Java, this would normally be a class. They provide confidence to programmers, allowing changes to be made. And by running tests throughout development, any changes that break the tests can be re-evaluated, whether that results in the production code being corrected or altering the test to make it pass. In this post, I will discuss the FIRST rules that are defined in the book Clean Code written by Uncle Bob Martin.
Related: How to Unit Test Java Stream Pipelines
FIRST means that tests should be:
- Fast: Tests should be fast enough that you won't be discouraged from using them. If you are making changes to a class or method that has a few tests attached to them, you are much more likely to run those tests after making changes if they take about a second to run, compared to slower tests that cannot only take a while to initialize, but each test case could also take over 1 second to run. For example, I have run integration tests that would take 20+ seconds to run and I hated having to run them after code changes. Unit tests, however, do not have to test multiple components like integration tests do and, therefore, keeping them running in under 1 second should be possible. DZone's previously covered how to implement the cucumber framework with Spring Boot integration tests.
- Independent: Tests should not depend on the state of the previous test, whether that is the state of an object or mocked methods. This allows you to run each test individually when needed, which is likely to happen if a test breaks, as you will want to debug into the method and not have to run the other tests before you can see what is going wrong. If you have a test that is looking for the existence of some data, then that data should be created in the setup of the test and preferably removed afterward so as not to affect the later tests.
- Repeatable: Tests should be repeatable in any environment without varying results. If they do not depend on a network or database, then it removes the possible reasons for tests failing, as the only thing they depend on is the code in the class or method that is being tested. If the test fails, then the method is not working correctly or the test is set up wrong — and nothing else.
- Self-validating: Each test will have a single boolean output of pass or fail. It should not be up to you to check whether the output of the method is correct each time the test is run. The test should tell you if the result of the test was valid or not. This is normally done using asserts such as assertTrue or assertEquals, which will cause the test to pass or fail depending on their results.
- Timely: Unit tests should be written just before the production code that makes the test pass. This is something that you would follow if you were doing TDD (Test Driven Development), but otherwise it might not apply. I personally do not use TDD and therefore I always write my tests after writing my production code. Although I can see the reasons for following this, I believe this is the rule that could be skipped if not appropriate.
Testing Tutorial: How to Setup Cypress Tests in Azure DevOps Pipeline
Below is a little unit test that I wrote that follows the rules above. Please note that this is just a simple test, and if you think there are scenarios that are being not tested, you are correct! (But the example is long enough already.)
@RunWith(JUnit4.class)
public class PersonValidatorTest {
private PersonValidator testSubject = new PersonValidator();
private Person person;
@Test
public void personCalledDanNewtonIsNotValid() {
person = new Person("Dan","Newton",23,1000);
assertFalse(testSubject.isValid(person));
}
@Test
public void personNotCalledDanNewtonIsValid() {
person = new Person("Bob","Martin",60,170);
assertTrue(testSubject.isValid(person));
}
@Test
public void personIsOlderThanTwentyFiveIsValid() {
person = new Person("Bob","Martin",60,170);
assertTrue(testSubject.isValid(person));
}
@Test
public void personIsYoungerThanTwentyFiveIsNotValid() {
person = new Person("Bob","Martin",10,170);
assertFalse(testSubject.isValid(person));
}
@Test
public void personIsTwentyFiveIsNotValid() {
person = new Person("Bob","Martin",25,170);
assertFalse(testSubject.isValid(person));
}
}
public class PersonValidator {
public boolean isValid(final Person person) {
return isNotCalledDanNewton(person) && person.getAge() > 25 && person.getHeight() < 180;
}
private boolean isNotCalledDanNewton(final Person person) {
return !person.getFirstName().equals("Dan") || !person.getLastName().equals("Newton");
}
}
So do these tests follow FIRST?
- Fast: They do not do much, so obviously they are quick to run.
- Independent: Each test sets up a new person and passes in all the parameters that are required for the test.
- Repeatable: This test does not depend on any other classes or require a connection to a network or database.
- Self-validating: Each test has a single assert, which will determine whether the test passes or fails.
- Timely: Failure!! I did not write these tests before writing the code in PersonValidator, but again, this is because I do not use TDD.
By following the FIRST rules in this post, your unit tests should see some improvement. I am not saying that following these rules alone will make your unit tests perfect, as there are still other factors that come into making a good test, but you will have a good foundation to build upon.
Published at DZone with permission of Dan Newton, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments