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
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
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);
}
}
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"})
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 classes are prepared for each layer. By preparing for each layer, you can capture only the required dependencies.
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
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);
}
}
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
}
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
}
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)",
})
AutoConfigureTestEntityManager Use annotations If so, TestEntityManager can be used without the DataJpaTest annotation.
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
}
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
}
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
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);
}
}
The test class is similar to the domain layer service test, so it will be omitted.
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
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);
}
}
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
}
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
}
AutoConfigureMockMvc Use annotations If you don't have the WebMvcTest annotation, you can use MockMvc.
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
}
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.
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
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.
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