Testing Best Practices for Java + Spring Apps

Max Mautner
Personal Capital Tech Blog
5 min readFeb 26, 2019

Whether you use Java, Spring, or any programming language — let’s reflect on why anyone writes software to test their own software.

After all, it sounds expensive!

Why test your software?

  • Verify that your software works
  • Protect against bugs
  • Reproduce bugs (in order to prove they can’t re-occur)
  • Customers represent value-at-risk!
  • As the business grows, more $$$ depends on your product working.
  • Testing protects value-at-risk.

Why test software (automatically)?

It’s cheaper than the alternatives:

  • Cost of servers vs. cost of labor
  • Cost of waiting on results (⏰)
  • Cost of non-determinism (flakiness)

It’s important to remember when working with manual or automated tests:

  • Slow tests are tests that aren’t run (i.e. ignored)
  • Flaky tests (tests that behave randomly) are ignored

In fact, the value of a test is correlated with its slowness/flakiness:

Image source

A test’s business value can even be negative if you don’t tread carefully:

That said, let’s review our 10 quick tips for test-writing best practices:

Table of Contents

1. Avoid void

Why return anything from a method?

Functions that return values are easier to test.

Why? Because you can make assertions about your method’s return value.

If you do use void then your tests will have to make assertions about the side effects of your method and your test will be tightly-coupled with the internal implementation of the method.

How can I test methods that use void?

Here are three ways:

  1. Use mocks to verify that a method was called and which arguments it was called with
  2. Verify that the unit under test had side effects on global state (e.g. record inserted into DB)
  3. Verify that a known exception was thrown

Why is void inferior to method’s that have return values?

void depends upon knowing the internal implementation of your unit under test.

Internal implementation changes (e.g. using PostgreSQL instead of MySQL)

2. Meaningful assertions

There is Junit’s suite of assertions:

Assert.assertNotNull(Object);Assert.assertTrue(boolean);Assert.assertTrue(String, boolean);

Which seems the most meaningful? The more specific your assertion, the better.

Even better is to adopt a readable assertions library like Google’s truth.

When asserting something, consider whether it is:

  • Specific: Does it assert one behavior? If not then consider splitting your test into multiple test methods
  • Understandable: what is the unit under test? Name your test according to the unit of business logic it is testing.

3. Expecting exceptions

Spot the difference between these 2 code snippets:

The first will always pass. The second is explicit and will fail if the expected ExceptionClass is not raised.

4. Hardcoding values in tests

DRY. Don’t Repeat Yourself

static final USER_EMAIL = …

Be aware of thread safety when re-using non-final class variables in tests. Mark them final to prevent race conditions that could change a variable shared across test methods.

5. Mockito basics (Part 1)

For deeper reading, read the docs!

This is an illustration of the basic patterns for mocking objects to avoid methods from being invoked during your test:

when(mockedObject.methodToMock(…)).thenReturn(value);doReturn(value).when(mockedObject).methodToMock(…)when(mockedObject.methodToMock(any())).thenReturn(value);doReturn(value).when(mockedObject).methodToMock(any())

Argument matchers:

  • Composite types: any(), any(Class.class)
  • Primitive types: anyInt(), anyList()

6. Mockito basics (Part 2)

If dealing with void methods, then you will have to opt for doNothing instead of doReturn/thenReturn:

doNothing().when(mock).method()

7. Mockito basics (Part 3)

An alternative assertion:

verify(mockObject, times(1)).methodToMock();

Was mockObject.methodToMock() called 1 time?

verify(mockObject, times(1)).methodToMock(eq(“Rohit”));

Was mockObject.methodToMock(…) called 1 time with “Rohit” as an argument?

8. Don’t Mock Too Much

We should consider the question:

If everything is mocked, are we really testing the code that runs in production?

That being said, mocking can often be avoided by:

  • not using void
  • using dependency injection (e.g. Spring)

9. Spring & Mockito

Spring is amenable to using the following Mockito annotations in lieu of @Autowired:

@InjectMocks, @Mock, @Spy, @RunWith(MockitoJunitRunner.class)

Example:

@RunWith(MockitoJUnitRunner.class)
public class SQLReportJobTest
{
@InjectMocks
@Spy
private SQLReportJob sqlReportJob;
@Test public void runWithNullGroupJobs() {
when(sqlReportJob.getParameterBean())
.thenReturn(parameterBean);
boolean thingy = sqlReportJob.getThingy();
assertTrue(thingy);
}
}

Criticisms of using @InjectMocks with Spring (1, 2, 3)

10. Where does slowness/flakiness comes from?

Slowness:

  • CPU (e.g. Spring dependency injection, Monte Carlo)
  • I/O
  • Network (downloading dependencies, interacting w/ datastores, APIs)
  • Disk (reading/writing files)
  • Thread.sleep

Flakiness:

  • Test “size” — large tests are more likely to be flaky
  • The larger the test, the more likely it will be flaky
  • Certain tools correlate with a higher rate of flaky tests (e.g. headless UI)
  • Size is more predictive than tool:
Image source

Conclusion

Automated tests have business value, and we recognize that at Personal Capital.

Following these testing best practices allows us to continue to deliver our product securely and reliably for our customers.

Interested in careers with Personal Capital? Get in touch!

Thanks are also due to the following team members for feedback in authoring this guide!

  • Rohit Iyer
  • Jethro Lai
  • Priyesh Kannan
  • Donya Izadi
  • Ankit Kumar

--

--