DEV Community

Cover image for Basic tip to improve the general quality of your tests and become a better developer
Carlos Monteiro
Carlos Monteiro

Posted on

Basic tip to improve the general quality of your tests and become a better developer

Author's message

Hello, developer community, I wrote this post based on insights gained over the years. This isn't something I’ve gleaned from books written by developers with decades of experience, so please don’t take these as rules. Use them if you agree, and share your ideas with me so I can learn from them.

Why are we testing?

Tests ensure that an application is working as expected. With coded tests, we can prevent anyone from changing this.

What do we as developers look for when testing?

When we are developing something, we know exactly what we expect as a result of the piece of code we have written. We have a clear understanding of it, what is happening, and what needs to happen in each possible scenario.

Our role is to be the guardian of functionality; no one can change the behavior we have gracefully programmed. Expected or unexpected changes must break the tests.

Are our tests good?

Let's start with two spring services:

The purpose of this first class is to provide the current offset date time using the given zone id.

@Service
public final class DateService {
 public OffsetDateTime getCurrentDate(final ZoneId zoneId) {
     return OffsetDateTime.now(zoneId);
 }
}
Enter fullscreen mode Exit fullscreen mode

This other class will be called by the controller layer and will use the DateService class to generate a date and before returning, it will format the result obtained.

@Service
public final class UtilityService {

    private final DateService dateGeneratorService;

    public static final String DEFAULT = "uuuu-MM-dd'T'HH:mmXXXXX";

    public UtilityService(final DateService dateGeneratorService) {
        this.dateGeneratorService = dateGeneratorService;
    }

    public String getNowDate(final String offsetId, final String format) {
        final OffsetDateTime dateTime = dateGeneratorService.getCurrentDate(
                Objects.requireNonNullElse(offsetId, ZoneOffset.UTC.getId())
        );

        return dateTime.format(DateTimeFormatter.ofPattern(
                Objects.requireNonNullElse(format, DEFAULT)
        ));
    }
}
Enter fullscreen mode Exit fullscreen mode

How about the test class?

@ExtendWith(MockitoExtension.class)
public class UtilityServiceTest {

    @Mock
    private DateService dateService;

    @InjectMocks
    private UtilityService utilityService;

    @Test
    public void shouldGetNowDateWithProvidedParameters() {
        when(dateService.getCurrentDate(any())).thenReturn(OffsetDateTime.now());

        final String result = utilityService.getNowDate(ZoneOffset.UTC, "uuuu-MM-dd'T'HH:mmXXXXX");

        assertNotNull(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

The above test is a common one. It will cover all lines (without considering all situations), check that the date service is being called (since the test is not throwing a NullPointerException), and return a result.

The test is working!

✓ shouldGetNowDateWithProvidedParameters()

What will happen if another developer tries to make changes during their daily routine? Let's exercise this:

[Scenario 1] Accidentally changed the parameter offsetId by format (since both are strings).

    public String getNowDate(final String offsetId, final String format) {
        final OffsetDateTime dateTime = dateGeneratorService.getCurrentDate(
                // BEFORE
                Objects.requireNonNullElse(offsetId, ZoneOffset.UTC.getId())
                // NOW
                Objects.requireNonNullElse(format, ZoneOffset.UTC.getId())
        );

        return dateTime.format(DateTimeFormatter.ofPattern(
                Objects.requireNonNullElse(format, DEFAULT)
        ));
    }
Enter fullscreen mode Exit fullscreen mode

The test is working!

✓ shouldGetNowDateWithProvidedParameters()

[Scenario 2] Consider that there is no reason to treat the null cases, since there is no reason to call API without one of the parameters.

    public String getNowDate(final String offsetId, final String format) {
        // BEFORE
        final OffsetDateTime dateTime = dateGeneratorService.getCurrentDate(
                Objects.requireNonNullElse(offsetId, ZoneOffset.UTC.getId())
        );
        // NOW
        final OffsetDateTime dateTime = dateGeneratorService.getCurrentDate(offsetId)
        );

        return dateTime.format(DateTimeFormatter.ofPattern(
                Objects.requireNonNullElse(format, DEFAULT)
        ));
    }
Enter fullscreen mode Exit fullscreen mode

The test is working!

✓ shouldGetNowDateWithProvidedParameters()

[Scenario 3] Decides to change the default pattern.

    // BEFORE
    public static final String DEFAULT = "uuuu-MM-dd'T'HH:mmXXXXX";
    // NOW
    public static final String DEFAULT = "uuuu-MM-dd'T";

    public String getNowDate(final String offsetId, final String format) {
        final OffsetDateTime dateTime = dateGeneratorService.getCurrentDate(
                Objects.requireNonNullElse(offsetId, ZoneOffset.UTC.getId())
        );

        return dateTime.format(DateTimeFormatter.ofPattern(
                Objects.requireNonNullElse(format, DEFAULT)
        ));
    }
Enter fullscreen mode Exit fullscreen mode

The test is working!

✓ shouldGetNowDateWithProvidedParameters()

How can we solve this?

The test class below works, but it is very inefficient.

@ExtendWith(MockitoExtension.class)
public class UtilityServiceTest {

    @Mock
    private DateService dateService;

    @InjectMocks
    private UtilityService utilityService;

    @Test
    public void shouldGetNowDateWithProvidedParameters() {
        when(dateService.getCurrentDate(any())).thenReturn(OffsetDateTime.now());

        final String result = utilityService.getNowDate("Z", "uuuu-MM-dd'T'HH:mmXXXXX");

        assertNotNull(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's create a new version!

Changes to prevent [Scenario 1].

    @Test
    public void shouldGetNowDateWithProvidedParameters() {
        final String anyOffsetId = "Z";
        final String anyFormat = "uuuu-MM-dd'T'HH:mmXXXXX";

        when(dateService.getCurrentDate(anyOffsetId)).thenReturn(OffsetDateTime.now());

        //act
        final String result = utilityService.getNowDate(anyOffsetId, anyFormat);

        assertNotNull(result);
    }
Enter fullscreen mode Exit fullscreen mode

✘ shouldGetNowDateWithProvidedParameters

We are not expecting to get the current date with any value; we know that the first parameter of the getNowDate method represents the offset that needs to be used to call the DateService class.

Changes to prevent [Scenario 2].

    @Test
    public void shouldUseUTCOffsetWhenGetNowDateIsCalledWithNull() {
        final String anyFormat = "uuuu-MM-dd'T'HH:mmXXXXX";

        when(dateService.getCurrentDate(ZoneOffset.UTC.getId())).thenReturn(OffsetDateTime.now());

        //act
        final String result = utilityService.getNowDate(null, anyFormat);

        assertNotNull(result);
    }
Enter fullscreen mode Exit fullscreen mode

✘ shouldGetNowDateWithProvidedParameters

Different scenarios need to be covered, the exact expectations need to be validated.

Changes to prevent [Scenario 3].

    @Test
    public void shouldUseDefaultPatternWhenGetNowDateIsCallWithNull() {
        final String anyOffsetId = "Z";
        final OffsetDateTime now = OffsetDateTime.now();

        when(dateService.getCurrentDate(anyOffsetId)).thenReturn(now);

        //act
        final String result = utilityService.getNowDate(anyOffsetId, null);

        assertEquals(
                result,
                now.format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mmXXXXX")),
                "The generated date string must have the pattern uuuu-MM-dd'T'HH:mmXXXXX"
        );
    }
Enter fullscreen mode Exit fullscreen mode

✘ shouldGetNowDateWithProvidedParameters

Different scenarios need to be covered, the exact expectations need to be validated.

Final Version

@ExtendWith(MockitoExtension.class)
public class UtilityServiceTest {

    @Mock
    private DateService dateService;

    @InjectMocks
    private UtilityService utilityService;

    @Test
    public void shouldGetNowDateWithProvidedParameters() {
        when(dateService.getCurrentDate(any())).thenReturn(OffsetDateTime.now());

        //act
        final String result = utilityService.getNowDate("Z", "uuuu-MM-dd'T'HH:mmXXXXX");

        assertNotNull(result);
    }

    /**
     * [Scenario 1]
     *
     * Accidentally changed the parameter offsetId by format (since both are strings).
     */
    @Test
    public void shouldGetNowDateWithProvidedParameters_Scenario_1() {
        final String anyOffsetId = "Z";
        final String anyFormat = "uuuu-MM-dd'T'HH:mmXXXXX";

        when(dateService.getCurrentDate(anyOffsetId)).thenReturn(OffsetDateTime.now());

        //act
        final String result = utilityService.getNowDate(anyOffsetId, anyFormat);

        assertNotNull(result);
    }

    /**
     * [Scenario 2]
     *
     * Consider that there is no reason to treat the null cases, since there is no reason to call API without one of the parameters.
     */
    @Test
    public void shouldUseUTCOffsetWhenGetNowDateIsCalledWithNull() {
        final String anyFormat = "uuuu-MM-dd'T'HH:mmXXXXX";

        when(dateService.getCurrentDate(ZoneOffset.UTC.getId())).thenReturn(OffsetDateTime.now());

        //act
        final String result = utilityService.getNowDate(null, anyFormat);

        assertNotNull(result);
    }

    /**
     * [Scenario 3]
     *
     * Decides to change the default pattern.
     */
    @Test
    public void shouldUseDefaultPatternWhenGetNowDateIsCallWithNull() {
        final String anyOffsetId = "Z";
        final OffsetDateTime now = OffsetDateTime.now();

        when(dateService.getCurrentDate(anyOffsetId)).thenReturn(now);

        //act
        final String result = utilityService.getNowDate(anyOffsetId, null);

        assertEquals(
                result,
                now.format(DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mmXXXXX")),
                "The generated date string must have the pattern uuuu-MM-dd'T'HH:mmXXXXX"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The examples and refactorings we have made show us that even the best tool can be inefficient if not used well. Avoid partial checks. Every line must be considered, every scenario, and every expectation must be met. This will strengthen your tests and make you a better developer.


Check the source code on github.com.


Before you go!

Follow for more ;)
Suggest new topics
Use the comments section to discuss the feature and >comment on real-world use cases to help others understand >its usage


Top comments (0)