[JAVA] About designing Spring Boot and unit test environment

Overview

This is a somewhat thought-provoking article that summarizes the thoughts on designing a unit test environment that is easy to implement and maintain in a Web application that uses Spring Boot. Since it is an article from design, it does not touch on how to write test code concretely.

environment

reference

Demo application

Project structure

How to cut the package

In this demo application, the following three subpackages are created for each layer. Dividing layers into subpackages makes it easier to create a test environment for each layer.

src.main.java
  |
  +--- com.example.demo                          //★package root
  |     |
  |     +--- Application.java                    //★Main Application Class
  |     |
  |     +--- domain                              //★ Domain layer
  |     |     |
  |     |     +--- DatasourceConfig.java         //★ Data source configuration
  |     |     |
  |     |     +--- entity                        //☆ Place JPA entity class
  |     |     |
  |     |     +--- repository                    //☆ Place JPA repository interface
  |     |     |                                      (spring-data-jpa)
  |     |     +--- service                       //☆ Place business logic class
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- external                            //★ External layer
  |     |     |
  |     |     +--- service                       //☆ Place business logic class
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- web                                 //★ Web layer
  |           |
  |           +--- WebMvcConfig.java             //★ WebMvc configuration
  |           +--- JacksonConfig.java            //★ Jackson configuration
  |           |
  |           +--- advice
  |           |     |
  |           |     +--- CustomControllerAdvice.java
  |           |
  |           +--- interceptor
  |           |     |
  |           |     +--- CustomHandlerInterceptor.java
  |           |
  |           +--- controller                     //☆ Place controller class
  |
src.java.resources
  |
  +--- application.yml


src.test.java
  |
  +--- com.example.demo
  |     |
  |     +--- domain
  |     |     |
  |     |     +--- DomainTestApplication.java    //★ Main Application Class for domain layer test
  |     |     |
  |     |     +--- entity
  |     |     |
  |     |     +--- repository
  |     |     |
  |     |     +--- service
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- external
  |     |     |
  |     |     +--- ExternalTestApplication.java  //★ Main Application Class for external layer test
  |     |     |
  |     |     +--- service
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- web
  |           |
  |           +--- WebTestApplication.java       //★ Web Tier Test Main Application Class
  |           |
  |           +--- controller
  |
src.test.resources
  |
  +--- application.yml

Entry point class

In Spring Boot, it is recommended to place the Main Application Class annotated with SpringBootApplication in the package root (or a package higher than other classes). Placing this class above other classes will automatically scan the subordinate component (with Component and Service annotations) and configuration classes.

14.2 Locating the Main Application Class We generally recommend that you locate your main application class in a root package above other classes

This class is general (common) content, so there is nothing special to mention.

Application


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

Domain layer configuration

The configuration class related to the domain layer is placed directly under the domain package. This example is a class that configures the data source. I haven't configured it in this code, but I'm assuming it's in this class if you need to customize the data source or transaction manager.

DatasourceConfig


import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EntityScan
public class DatasourceConfig {

  // nothing

}

You can also specify the packages to scan in basePackages, which is omitted in the above code, as shown below.

@EnableJpaRepositories(basePackages = {"com.example.demo.domain.repository"})
@EntityScan(basePackages = {"com.example.demo.domain.entity"})

Web tier configuration

The configuration class related to the web layer is placed directly under the web package. This example is a class that customizes WebMvc and

WebMvcConfig


@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CustomHandlerInterceptor())
            .addPathPatterns("/memo/**");
    }

}

This is a class for customizing Jackson. The code below customizes the ObjectMapper, but you can do the same with the configuration file.

JacksonConfig


@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.serializationInclusion(JsonInclude.Include.NON_NULL)
            .indentOutput(true)
            .failOnUnknownProperties(false)
            .failOnEmptyBeans(false)
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToEnable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS);
        return builder;
    }

}

Test code

Entry point class

Test code entry point classes are prepared for each layer. By preparing for each layer, you can capture only the required dependencies.

Domain layer testing

Domain tier test dependency range

The domain tier test package root is com.example.demo.domain, and the DomainTestApplication class is the entry point for testing.

src.main.java
  |
  +--- com.example.demo
  |     |
  |     +--- domain
  |           |
  |           +--- DatasourceConfig.java  //★ Data source configuration
  |           |
  |           +--- entity
  |           |
  |           +--- repository
  |           |
  |           +--- service
  |                 |
  |                 +--- impl
  |
src.java.resources
  |
  +--- application.yml


src.test.java
  |
  +--- com.example.demo
  |     |
  |     +--- domain
  |           |
  |           +--- DomainTestApplication.java  //★ Main Application Class for domain layer test
  |           |
  |           +--- entity
  |           |
  |           +--- repository
  |           |
  |           +--- service
  |                 |
  |                 +--- impl
  |
src.test.resources
  |
  +--- application.yml

Main Application Class for testing

DomainTestApplication


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DomainTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(DomainTestApplication.class, args);
    }
}

Repository unit test (using embedded database)

DataJpaTest for unit test of repository Use annotations. If you add DataJpaTest annotation, the embedded database will be used regardless of the data source settings. (H2 is used in this demo application) Also, instead of EntityManager, TestEntityManager Is available.

@RunWith(SpringRunner.class)
@DataJpaTest
public class MemoRepositoryTests {

    @Autowired
    private TestEntityManager testEntityManager;
    @Autowired
    private MemoRepository memoRepository;

    //Test code

}

Repository integration test (using defined data source)

If you want to perform integration test using data source defined in configuration file instead of embedded DB, [AutoConfigureTestDatabase](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/ org / springframework / boot / test / autoconfigure / jdbc / AutoConfigureTestDatabase.html) You can change the setting with the annotation.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemoRepositoryIntegrationTests {

    @Autowired
    private TestEntityManager testEntityManager;
    @Autowired
    private MemoRepository memoRepository;

    //Test code

}
Input test data

If you want to use sql file or sql statement to input test data, [Sql](https://docs.spring.io/spring-framework/docs/5.0.4.RELEASE/javadoc-api/org/springframework/test /context/jdbc/Sql.html) Annotations are available. Annotations can be attached to classes and methods, but if they are attached to both as described in JavaDoc, the method settings take precedence.

Method-level declarations override class-level declarations.

@Sql(statements = {
    "INSERT INTO memo (id, title, description, done, updated) VALUES (11, 'test title 1', 'test description', false, CURRENT_TIMESTAMP)",
    "INSERT INTO memo (id, title, description, done, updated) VALUES (12, 'test title 2', 'test description', true, CURRENT_TIMESTAMP)",
    "INSERT INTO memo (id, title, description, done, updated) VALUES (13, 'test title 3', 'test description', false, CURRENT_TIMESTAMP)",
})
Use TestEntityManager other than DataJpaTest

AutoConfigureTestEntityManager Use annotations If so, TestEntityManager can be used without the DataJpaTest annotation.

Service unit test

Spring Runner is not required for unit testing of services because it does not depend on Spring Framework. The components that the test target depends on are mocked (or spyed) in Mockito.

public class MemoServiceImplTests {

    @Rule
    public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);

    @Mock
    private MemoRepository memoRepository;
    @InjectMocks
    private MemoServiceImpl sut;

    //Test code

}

Service integration test

Use SpringBootTest annotation for service integration test To do. Also, since the Web server function is not required, WebEnvironment.NONE is specified for webEnvironment. If set to NONE, the built-in web server will not start. The data source of the configuration file is used to access the database. To input test data, you can use the above Sql annotation or EntityManager with the following code. Other methods described in 80. Database Initialization There is.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@Transactional
public class MemoServiceImplIntegrationTests {

    @Autowired
    private EntityManager entityManager;
    @Autowired
    private MemoServiceImpl sut;

    //Test code

}

External layer testing

Dependency (impact) range of external layer test

The external tier test package root is com.example.demo.external and the ExternalTestApplication class is the entry point for testing.

src.main.java
  |
  +--- com.example.demo
  |     |
  |     +--- external
  |           |
  |           +--- service
  |                 |
  |                 +--- impl
  |
src.java.resources
  |
  +--- application.yml


src.test.java
  |
  +--- com.example.demo
  |     |
  |     +--- external
  |           |
  |           +--- ExternalTestApplication.java  //★ Main Application Class for external layer test
  |           |
  |           +--- service
  |                 |
  |                 +--- impl
  |
src.test.resources
  |
  +--- application.yml

Main Application Class for testing

Exclude DataSourceAutoConfiguration from AutoConfiguration because the external tier is database independent.

ExternalTestApplication


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class
})
public class ExternalTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExternalTestApplication.class, args);
    }
}

Service unit, integration test

The test class is similar to the domain layer service test, so it will be omitted.

Web tier testing

Web tier test dependency range

The web tier test package root is com.example.demo.web, and the WebTestApplication class is the entry point for testing.

src.main.java
  |
  +--- com.example.demo
  |     |
  |     +--- domain
  |     |     |
  |     |     +--- DatasourceConfig.java  //★ Data source configuration
  |     |     |
  |     |     +--- entity
  |     |     |
  |     |     +--- repository
  |     |     |
  |     |     +--- service
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- external
  |     |     |
  |     |     +--- service
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- web
  |           |
  |           +--- WebMvcConfig.java  //★ WebMvc configuration
  |           +--- JacksonConfig.java  //★ Jackson configuration
  |           |
  |           +--- advice
  |           |     |
  |           |     +--- MyControllerAdvice.java
  |           |
  |           +--- interceptor
  |           |     |
  |           |     +--- MyHandlerInterceptor.java
  |           |
  |           +--- controller
  |
src.java.resources
  |
  +--- application.yml


src.test.java
  |
  +--- com.example.demo
  |     |
  |     +--- domain
  |     |     |
  |     |     +--- entity
  |     |     |
  |     |     +--- repository
  |     |     |
  |     |     +--- service
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- external
  |     |     |
  |     |     +--- service
  |     |           |
  |     |           +--- impl
  |     |
  |     +--- web
  |           |
  |           +--- WebTestApplication.java  //★ Web Tier Test Main Application Class
  |           |
  |           +--- controller
  |
src.test.resources
  |
  +--- application.yml

Main Application Class for testing

Since the Web layer depends on the domain layer and the external layer, the target package is specified in scanBasePackages.

WebTestApplication


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = {
    "com.example.demo.domain.service",
    "com.example.demo.external.service",
    "com.example.demo.web"
})
public class WebTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebTestApplication.class, args);
    }
}

Unit test to convert entity to Json

JsonTest annotation in Json unit test I will use it. It is a unit test to see if the result of converting from entity to json is serialized as expected. Test for infinite loops if you customize serialization with Jackson's JsonProperty or JsonIgnore annotations, or if there are cross-references between entities, especially with JPA related annotations (OneToMany, ManyToOne, etc.).

@RunWith(SpringRunner.class)
@JsonTest
public class MemoToJsonTests {

    @Autowired
    private JacksonTester<Memo> json;

    //Test code

}

Controller unit test

WebMvcTest for unit test of controller Use annotations. WebMvcTest can use MockMvc and WebClient (requires HtmlUnit for dependency).

Mocking the components that the controller under test depends on is Spring Boot's [MockBean](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test /mock/mockito/MockBean.html) (Spy bean [SpyBean](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/mock /mockito/SpyBean.html)) Use annotations. Objects mocked by MockBean are added to the application context and injected into the test target (MemoController in this example).

@RunWith(SpringRunner.class)
@WebMvcTest(MemoController.class)
public class MemoControllerTests {

    @Autowired
    private MockMvc mvc;
    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private MemoService memoService;

    //Test code

}
Use MockMvc other than WebMvcTest

AutoConfigureMockMvc Use annotations If you don't have the WebMvcTest annotation, you can use MockMvc.

Controller integration test

In test classes that use SpringBootTest annotations, instead of RestTemplate, [TestRestTemplate](https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/api/org/springframework/boot/test/web /client/TestRestTemplate.html) is available. Import the DatasourceConfig class because the controller integration test requires access to the database.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(value = {DatasourceConfig.class})
public class MemoControllerIntegrationTests {

    @Autowired
    private TestRestTemplate testRestTemplate;

    //Test code

}
Replacing data sources during integration test

By defining a setting class like the code below, you can replace the data source with the embedded DB in any integration test.

@TestConfiguration
public class WebTestConfig {

    @Bean
    public DataSource datasource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .setScriptEncoding("UTF-8")
                .addScripts("classpath:scripts/init.sql")
                .build();
    }

}

TestConfiguration The configuration class using annotation is SpringBootConfiguration It is not subject to automatic annotation detection and must be imported manually.

@Import(value = {DatasourceConfig.class, WebTestConfig.class})

By the way, when creating a schema or inputting test data, it is difficult to use because it must be described in SQL that can be used even in an embedded database.

Supplement

Check automatic configuration

If you set the debug property in the configuration file, the debug log will be output. (The same applies if you specify -Ddebug in the system properties)

debug: true

The result of the automatic configuration "CONDITIONS EVALUATION REPORT" is output to the debug log, so you can check the configuration status.

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   AopAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'org.springframework.context.annotation.EnableAspectJAutoProxy', 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice', 'org.aspectj.weaver.AnnotatedElement'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)

//abridgement

Negative matches:
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)

//abridgement

Exclusions:
-----------

    None

Unconditional classes:
----------------------

    org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

//abridgement

Replace the private field under test

Mockito.Whitebox is not available

In unit tests with Mockito 1.x, I sometimes saw test code that replaced the private field under test with a class called Whitebox, but since Mockito 2.1 this class is no longer available.

Whitebox.setInternalState(sut, "someService", mockSomeService);

In Spring (Boot), the Value annotation may inject the setting value into the private field, so I have seen it used in such cases as well.

@Value("${app.someValue}")
private String someValue;

The reason why the Mockito development team removed Whitebox can be found in the following issues, but if you make a rough translation, "Easy use of Whitebox has boosted the mass production of poor quality test code. It seems that it is from.

Alternative method

In Spring (Boot), instead of Whitebox ReflectionTestUtils You can use classes, but if replacement with reflection is bad, you're hesitant to use this method as well.

Another alternative is to change the visibility of the field from private to package private. Since the test class is usually in the same package as the class under test, it is possible to rewrite the fields directly from the test code. It's not uncommon to extend the visibility to make testing easier, for example VisibleForTesting for Google Guava. There is an annotation (/google/common/annotations/VisibleForTesting.html). Since this annotation is a marker annotation, it does not automatically expand the visible range during testing.

I'm not familiar with test-driven development and unit testing techniques, so I can't comment on the fact that using reflection is "bad" and loosening the visibility is "better than".

Recommended Posts

About designing Spring Boot and unit test environment
Spring boot development-development environment-
Switch environment with Spring Boot application.properties and @Profile annotation
How to write a unit test for Spring Boot 2
[Spring Boot] Environment construction (macOS)
Docker × Spring Boot environment construction
Use Spring Test + Mockito + JUnit 4 for Spring Boot + Spring Retry unit tests
How to unit test Spring AOP
◆ Spring Boot + gradle environment construction memo
Write test code in Spring Boot
Spring profile function, and Spring Boot application.properties
About app testing RSpec (unit test)
Use DBUnit for Spring Boot test
Sample code to unit test a Spring Boot controller with MockMvc
Spring Flash Scope Redirection and Unit Testing
JUnit unit test pre-processing and post-processing order
Create Spring Boot development environment on Vagrant
Introduce RSpec and write unit test code
Spring Boot environment construction memo on mac
About Spring ③
Perform transaction confirmation test with Spring Boot
HTTPS with Spring Boot and Let's Encrypt
Spring Boot @WebMvcTest test enables Spring Security default security
Spring Boot + Docker Java development environment construction
Try Spring Boot 1 (Environment construction ~ Tomcat startup)
Form class validation test with Spring Boot
Add spring boot and gradle to eclipse
Spring Boot environment construction with Docker (January 2021 version)
Spring Boot Whitelabel Error Page and JSON Response
Test controller with Mock MVC in Spring Boot
Summary of what I learned about Spring Boot
Output request and response log in Spring Boot
Various correspondence table of Spring Framework and Spring Boot
Build Spring Boot project by environment with Gradle
Create Spring Boot environment with Windows + VS Code
Spring Boot + Spring Data JPA About multiple table joins
Create a Spring Boot development environment with docker
About Spring AOP
Challenge Spring Boot
About spring AOP
Spring Boot Form
[Spring] Environment construction
Spring Boot Memorandum
gae + spring boot
Plans to support JDK 11 for Eclipse and Spring Boot
Spring Boot application built-in Tomcat, Apache and WebSocket integration
Try using DI container with Laravel and Spring Boot
Spring Security usage memo: Cooperation with Spring MVC and Boot
[JUnit 5 compatible] Write a test using JUnit 5 with Spring boot 2.2, 2.3
Spring Boot with Spring Security Filter settings and addictive points
[Spring boot] I thought about testable code by DI
SSO with GitHub OAuth in Spring Boot 1.5.x environment
[JUnit 5] Write a validation test with Spring Boot! [Parameterization test]
Test field-injected class in Spring boot test without using Spring container
WebAPI unit test and integration test with SpringBoot + Junit5, 4 patterns
How to boot by environment with Spring Boot of Maven
Attempt to SSR Vue.js with Spring Boot and GraalJS
I wrote a test with Spring Boot + JUnit 5 now
Database environment construction with Docker in Spring boot (IntellJ)
Connect Spring Boot and Angular type-safely with OpenAPI Generator
Hello World comparison between Spark Framework and Spring Boot