Double Testing

Write your tests once and run them twice - as both unit and integration tests - sounds like a good deal, let's take a look at this practice.


This article focuses on Java, Spring framework and Maven.

We have a modular application with two independent String MVC web components. We created a Spring Boot starter for each component to bring them together in a monolithic Spring Boot application.

Each web component has its own set of tests. We have some REST-Assured tests for REST-based endpoints and Selenium-based tests for testing requests in the end-to-end manner (we check whether expected elements are present on the page).

To run the tests independently as a part of the component build process we have to simulate the web environment and to mock (or somehow else provide) all needed resources. We can achieve this with @SpringBootTest:

@SpringBootTest(
    classes = IndexController.class /* this is what we test */,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
@EnableAutoConfiguration
class IndexTest {

  @Value("http://localhost:${local.server.port:8080}")
  private String url;

  @Test
  void indexResource_shouldContainWeb1() {
    given()
        .baseUri(url)
        .basePath("/").
    when()
        .get().
    then()
        .statusCode(200)
        .contentType("text/plain")
        .body(containsString("Web1"));
  }
}

If you don't (or can't) include @EnableAutoConfiguration you can create your own annotation with a minimal web configuration from spring-boot-starter-web:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Import({
    ServletWebServerFactoryAutoConfiguration.class,
    DispatcherServletAutoConfiguration.class,
    WebMvcAutoConfiguration.class
})
@interface MinimalWebConfiguration {
}

To be sure that a component works in the application as well as running standalone, we can use the same tests, this time executed against the application. Let's call them integration tests and run them in a further phase of the application delivery process.

Let's create an abstract class containing the whole test code:

abstract class IndexTestBase {
  @Test
  void indexResource_shouldContainWeb1() {
    // the test code...
  }
}

Standalone (unit) tests use this base:

// Spring Boot settings as above...
class IndexTest extends IndexTestBase {
  // noting else in this class
}

As well as the integration tests:

@SpringJUnitConfig
class IndexIT extends IndexTestBase {
  @Configuration
  static class ITConfig {
    // intentionally empty
  }
}

Alternatively to @SpringJUnitConfig you can use @ExtendWith(SpringExtension.class) together with @ContextConfiguration to gain a better overlook.

IndexTest is executed by Maven Surefire Plugin in the test phase, but IndexIT integration test is ignored by Surefire because it doesn't match the default naming pattern (*Test).

Application Integration

Web components are integrated into the application by adding their Spring Boot Starters as dependencies:

<dependency>
  <groupId>com.ttulka.samples.doubletesting</groupId>
  <artifactId>sample-component-web1-spring-boot-starter</artifactId>
</dependency>

And setting different URL paths:

@RestController("indexControllerWeb1")	/* unique name to avoid a bean collision */
@RequestMapping("${doubletesting.path.web1:}")	/* integrated under an unique url path */
public class IndexController {
  // endpoints definition...
}
# application.yml
doubletesting.path:
  web1: web1
  web2: web2

Running Integration Tests

Integration tests should run in a later phase of the application delivery process than unit tests. The reason is, integration tests are generally slower than unit tests (mainly due to time needed for building the environment) and running them in an earlier phase would delay the feedback, which is highly undesirable.

We create a separate Maven module not included in the build phase at all, skipped with -DskipITs or alternatively by settings of a build profile.

To run the tests from a component, first we have to collect them and include as a dependency. We use Maven JAR Plugin and its goal test-jar for that:

<!-- pom.xml of a component -->
<build>
  <plugins>
    <plugin>
      <artifactId>maven-jar-plugin</artifactId>
      <executions>
        <execution>
          <id>test-jar</id>
          <goals>
            <goal>test-jar</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

The plugin collects all the tests as a separate artifact, we then include in the integration test module:

<!-- pom.xml of the integration tests module -->
<dependencies>
  <dependency>
    <groupId>com.ttulka.samples.doubletesting</groupId>
    <artifactId>sample-component-web1</artifactId>
    <version>0</version>
    <type>test-jar</type>
    <scope>test</scope>
  </dependency>
  <!-- other dependencies... -->
</dependencies>

Then, all we need is to copy test classes with Maven Dependency Plugin and prepare the environment. We use Maven Failsafe Plugin and its phases pre-integration-test to start the application and post-integration-test to stop it and clean resources.

IndexIT will be executed automatically because it matches Failsafe naming pattern (*IT). Alternatively we can use tagging from JUnit 5 (@Tag("integration")) together with Failsafe configuration (groups) for a finer control of test executions.

We have to include all the test dependencies into the test pom.xml as dependencies with scope test don't come transitively.

Double Testing

A working sample project can be found on my Github.

And that's it! One test code base, two different executions - unit and integration.

Keep on testing!