Skip to content

API Best Practices

CH Albach edited this page Nov 9, 2021 · 2 revisions

Unit testing

JUnit test basics

Our codebase uses JUnit 5. While most tests depend on Spring injection; this is not a requirement. For testing pure static utilities or MapStruct mappers, plain JUnit tests will often suffice (i.e. no Spring annotations on the test class).

Assertions

Prefer the Google Truth library for assertions.

Parameterized test cases

In cases of repetitive test execution/assertions, consider using a table-driven @ParameterizedTest. In most cases, you'll want to use the reflection-based custom arguments provider approach:

Spring testing

Our Spring tests make heavy use of Spring Dependency Injection. A lack of basic knowledge on this subject will make it difficult to understand the test configuration. Minimally, it is good to have a general understanding of what @Autowired and @Beando.

  • Annotate the test class with @SpringJUnitConfig
  • In most cases, create an inner @TestConfiguration class
    • Note: it is possible to get similar utility by applying import annotations to the test class directly, but splitting these annotations from other Test class annotations generally provides more clarity.
  • Import concrete implementation dependencies
  • @Autowired the subject of your test
  • If needed, mock any bean dependencies via @MockBean
  • If needed, @Autowired any mocks or dependencies which your test needs to directly access

Typical Spring unit test boilerplate:

@SpringJUnitConfig
public class MyTest {
  @TestConfiguration
  @Import({MyTestSubject.class, MyServiceImpl.class})
  @MockBean({MyOtherService.class})
  static class TestConfiguration {
    @Bean
    public MyBean myBeanIfNeeded() [
      return new MyBean();
    }
  }

  @Autowired private MyTestSubject myTestSubject;
  @Autowired private MyOtherService mockMyOtherService;

  @Test
  public void testFoo() {
    ...
  }
}

Which dependencies do I need?

You need to provide beans to satisfy all transitive dependencies of the beans you are injecting into your test. Typically this means all dependencies of the subject under test.

Example scenario:

  • I'm testing an API controller
  • I @Import the controller class
  • I @Import or @MockBean all dependencies to satisfy the @Autowired controller constructor - or if no constructor, all @Autowired instance fields.
  • For anything I @Importd, I @Import or @MockBean any of its dependencies
  • repeat

Spring will quickly complain about "cannot find bean" when running your test if it's missing a dependency. It will typically take many iterations to resolve all of these import issues.

DAOs

To autowire DAOs into your test (these will use an in-memory H2 database), use @DataJpaTest on your test class. Note: in most cases this should subsume the functionality of @SpringJUnitConfig and can replace that.

@DataJpaTest
public class MyTest {
  @Autowired private WorkspaceDao workspaceDao;
  ...

Time

In your Java code, avoid global functions for determining the current time. Instead, inject a Clock in the encapsulating service or controller. From tests, you can then stub out the current time by injecting an instance of FakeClock. The FakeClockConfiguration sets this up for you. To control the time, just inject the FakeClock instance.

  ...
  @TestConfiguration
  @Import({FakeClockConfiguration.class})
  static class TestConfiguration {}

  @Autowired private FakeClock fakeClock;

  @Test
  public void myTest() {
    fakeClock.set(TIME_0);
  }

TODO: Add the error message here for when you forget to import FakeClockConfiguration

Spring Data JPA time testing

In a few cases, we use annotations to automatically set timestamps on our Hibernate models: @CreatedDate, @LastModifiedDate. In order to fake out the times for these interactions, import FakeJpaDateTimeConfiguration - in addition to the usual FakeClockConfiguration. Set the clock time as described above.

  ...
  @TestConfiguration
  @Import({FakeClockConfiguration.class, FakeJpaDateTimeConfiguration.class})
  static class TestConfiguration {}