This is an explanatory article when creating a Rest API application using Spring Boot in a maven multi module configuration project. The first half of the article describes the logical (pom description) and physical (directory / file structure description) structure of the multi-module project, and the second half supplements the features of each module.
environment
reference
We will use the Rest API application, which consists of three modules, as the subject of the multi-module structure project.
module | Root package | Description |
---|---|---|
application | com.example.application | Implemented communication processing with clients such as controllers. Depends on the domain module. |
domain | com.example.domain | Implement data access (entities and repositories) and business logic (services). Depends on the common module. |
common | com.example.common | Implement common processing such as utilities. |
There are four pom.xml in total, one for each project and one for each module. The description of each pom is as follows.
The pom of the parent project sets the project information, defines the module, and defines the dependencies required for the entire project.
no | point |
---|---|
1 | Specify rubber for packaging |
2 | spring to parent-boot-starter-Specify parent |
3 | For modules, specify the modules that make up the project. |
4 | Dependencies define the dependencies required by each module. The library defined here is available to all modules |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- point.1 -->
<packaging>pom</packaging>
<name>mmsbs</name>
<description>Multi Modules Spring Boot Sample application</description>
<!-- point.2 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/>
</parent>
<!-- point.3 -->
<modules>
<module>application</module>
<module>domain</module>
<module>common</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<!-- point.4 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
no | point |
---|---|
1 | Specify the parent project in parent |
2 | Dependencies define the dependencies required by the application module |
3 | Build settings are described in pom of application module |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>application</name>
<description>Application Module</description>
<!-- point.1 -->
<parent>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- point.2 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>mmsbs</finalName>
<plugins>
<!-- point.3 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludeDevtools>true</excludeDevtools>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<compilerVersion>1.8</compilerVersion>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<!--<arg>-verbose</arg>-->
<arg>-Xlint:all,-options,-path</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
no | point |
---|---|
1 | Specify the parent project in parent |
2 | dependencies defines the dependencies required by the domain module |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>domain</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>domain</name>
<description>Domain Module</description>
<!-- point.1 -->
<parent>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- point.2 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-java8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
no | point |
---|---|
1 | Specify the parent project in parent |
2 | Dependencies define the dependencies required by the common module, but they are not specified because there is nothing required so far. |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>common</name>
<description>Common Module</description>
<!-- point.1 -->
<parent>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- point.2 -->
<dependency>
</dependency>
</project>
Execute the following mvn command in the project directory to build.
> mvn clean package
Add the following options to skip the test.
> mvn clean package -Dmaven.test.skip=true
The artifacts are output under the target directory of each module. The artifact (executable jar) as a Spring Boot application is created in the target directory of the application module. To execute it, execute the following command.
> java -jar application\target\mmsbs.jar
...abridgement...
Tomcat started on port(s): 8080 (http)
Started Application in 14.52 seconds (JVM running for 15.502)
** Rest API execution example **
> curl -X GET http://localhost:8080/memo/id/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 72 0 72 0 0 4800 0 --:--:-- --:--:-- --:--:-- 4800
{
"title": "memo shopping",
"description": "memo1 description",
"done": false
}
The physical directory / file structure of the project is as follows. The project directory is a simple structure that stores pom.xml for the project and the directory of each module. If you manage your project with git, the .git repository directory will also be here. The directory of each module has the structure of a normal maven project.
/mmsbs
|
+--- /.git
+--- pom.xml
+--- README.md
|
+--- /application <---(1)
| |
| +--- pom.xml
| |
| +--- /src
| | |
| | +--- /main
| | | |
| | | +--- /java
| | | | |
| | | | +--- /com.example
| | | | |
| | | | +--- Application.java <---(2)Application entry point
| | | | +--- WebMvcConfigure.java
| | | | |
| | | | +--- /application
| | | | |
| | | | +--- /config <---(4)
| | | | +--- /controller <---(5)
| | | | +--- /interceptor
| | | | +--- /vo <---(6)
| | | |
| | | +--- /resources
| | | |
| | | +--- application.yml <---(3)Application configuration file
| | | +--- logback-spring.xml
| | | +--- messages.properties
| | |
| | +--- /test
| | |
| | +--- /java
| | | |
| | | +--- /com.example
| | | |
| | | +--- /application
| | | |
| | | +--- /controller <---(7,8)
| | | +--- /vo <---(9)
| | |
| | +--- /resources
| |
| +--- /target
| |
| +--- mmsbs.jar <---(10) executable jar
|
+--- /domain <---(11)
| |
| +--- pom.xml
| |
| +--- /src
| | |
| | +--- /main
| | | |
| | | +--- /java
| | | |
| | | +--- /com.example.domain
| | | |
| | | +--- /config <---(12)
| | | +--- /datasource <---(13)
| | | +--- /entity <---(14)
| | | +--- /repository <---(15)
| | | +--- /service <---(16)
| | | |
| | | +--- /impl <---(16)
| | +--- /test
| | |
| | +--- /java
| | | |
| | | +--- /com.example.domain
| | | |
| | | +--- TestApplication.java <---(17) for testing
| | | |
| | | +--- /repository <---(19,20)
| | | +--- /service <---(21,22)
| | |
| | +--- /resources
| | |
| | +--- application.yml <---(18) for testing
| |
| +--- /target
| |
| +--- domain-0.0.1-SNAPSHOT.jar
|
+--- /common <---(23)
|
+--- pom.xml
|
+--- /src
| |
| +--- /main
| | |
| | +--- /java
| | |
| | +--- /com.example.common
| | |
| | +--- /confing <---(24)
| | +--- /util <---(25)
| +--- /test
| |
| +--- /java
| | |
| | +--- /com.example.common
| | |
| | +--- /util <---(26)
| |
| +--- /resources
|
+--- /target
|
+--- common-0.0.1-SNAPSHOT.jar
There is no special implementation in a multi-module configuration, it is an entry point class for ordinary Spring Boot applications.
Application
package com.example;
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);
}
}
In addition to the Spring Boot settings, the settings specific to each module are also described. Considering the independence of the module, I think that it is better to place the module-specific setting values in the module, but prioritizing convenience, they are summarized in application.yml. Since it is a sample, the setting value has no particular meaning.
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/sample_db
username: test_user
password: test_user
driverClassName: com.mysql.jdbc.Driver
tomcat:
maxActive: 4
maxIdle: 4
minIdle: 0
initialSize: 4
jpa:
properties:
hibernate:
# show_sql: true
# format_sql: true
# use_sql_comments: true
# generate_statistics: true
jackson:
serialization:
write-dates-as-timestamps: false
server:
port: 8080
logging:
level:
root: INFO
org.springframework: INFO
# application settings
custom:
application:
key1: app_a
key2: app_b
key3: ajToeoe04jtmtU
domain:
key1: domain_c
key2: domain_d
common:
key1: common_e
key2: common_f
datePattern: yyyy-MM-dd
It is assumed that the class that holds the config information referenced by the class implemented in the application module. Read the settings from the application.yml file.
AppConfigure
package com.example.application.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@ConfigurationProperties(prefix = "custom.application")
@Data
@Slf4j
public class AppConfigure {
private String key1;
private String key2;
//private String key3;
@PostConstruct
public void init() {
log.info("AppConfigure init : {}", this);
}
}
It is a simple API that just responds (processes the data of the Memo table into a view object). The controller depends on the MemoService implemented in the domain module and the AppConfigure class in the application module. By the way, the handler method called id2 is a version that changes the return value of the handler method called id (without using ResponseEntity).
MemoController
package com.example.application.controller;
import com.example.application.config.AppConfigure;
import com.example.application.vo.MemoView;
import com.example.domain.entity.Memo;
import com.example.domain.service.MemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping(path = "memo", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Slf4j
public class MemoController {
@Autowired
private MemoService service;
@Autowired
private AppConfigure config;
@Value("${custom.application.key3}")
private String key3;
@PostConstruct
public void init() {
log.info("MemoController init : config.key1:{}, config.key2:{}, key3:{}", config.getKey1(), config.getKey2(), key3);
}
@GetMapping(path = "id/{id}")
public ResponseEntity<MemoView> id(@PathVariable(value = "id") Long id) {
log.info("id - id:{}, config.key1:{}, config.key2:{}, key3:{}", id, config.getKey1(), config.getKey2(), key3);
Memo memo = service.findById(id);
return new ResponseEntity<>(convert(memo), HttpStatus.OK);
}
//Pattern that does not use ResponseEntity
@GetMapping(path = "id2/{id}")
@ResponseBody
public MemoView id2(@PathVariable(value = "id") Long id) {
log.info("id2 - id:{}, config.key1:{}, config.key2:{}, key3:{}", id, config.getKey1(), config.getKey2(), key3);
Memo memo = service.findById(id);
return convert(memo);
}
@GetMapping(path = "title/{title}")
public ResponseEntity<List<MemoView>> title(@PathVariable(value = "title") String title, Pageable page) {
Page<Memo> memos = service.findByTitle(title, page);
return new ResponseEntity<>(convert(memos.getContent()), HttpStatus.OK);
}
private MemoView convert(final Memo memo) {
return MemoView.from(memo);
}
private List<MemoView> convert(final List<Memo> memos) {
return memos.stream()
.map(MemoView::from)
.collect(Collectors.toList());
}
}
This class is supposed to be a view object that has information that responds to the client. In this application, the entity is not returned as it is, but is converted to a view object once.
MemoView
package com.example.application.vo;
import com.example.domain.entity.Memo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;
import java.io.Serializable;
@Value
@Builder
public class MemoView implements Serializable {
private static final long serialVersionUID = -6945394718471482993L;
private String title;
private String description;
@JsonProperty("completed")
private Boolean done;
public static MemoView from(final Memo memo) {
return MemoView.builder()
.title(memo.getTitle())
.description(memo.getDescription())
.done(memo.getDone())
.build();
}
}
This is a sample implementation of this controller when unit testing it. Classes that the controller depends on are mocked with the MockBean annotation. Also, it does not connect to the database when running the test.
MemoControllerTests
package com.example.application.controller;
import com.example.application.config.AppConfigure;
import com.example.application.vo.MemoView;
import com.example.domain.entity.Memo;
import com.example.domain.service.MemoService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@RunWith(SpringRunner.class)
@WebMvcTest(value = MemoController.class, secure = false)
public class MemoControllerTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MemoService service;
@MockBean
private AppConfigure config;
private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
@Before
public void setup() {
Mockito.when(config.getKey1()).thenReturn("TEST_APP_VALUEA");
Mockito.when(config.getKey2()).thenReturn("TEST_APP_VALUEB");
}
@Test
public void test_id() throws Exception {
Long id = 1L;
LocalDateTime updated = LocalDateTime.of(2017, 9, 20, 13, 14, 15);
Memo expected = Memo.builder().id(id).title("memo").description("memo description").done(false).updated(updated).build();
Mockito.when(service.findById(Mockito.anyLong())).thenReturn(expected);
RequestBuilder builder = MockMvcRequestBuilders
.get("/memo/id/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = mockMvc.perform(builder)
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$").isNotEmpty())
.andExpect(jsonPath("$.title").value(expected.getTitle()))
.andExpect(jsonPath("$.description").value(expected.getDescription()))
.andExpect(jsonPath("$.completed").value(expected.getDone()))
.andDo(print())
.andReturn();
}
@Test
public void test_id2() throws Exception {
Long id = 1L;
LocalDateTime updated = LocalDateTime.of(2017, 9, 20, 13, 14, 15);
Memo expected = Memo.builder().id(id).title("memo").description("memo description").done(false).updated(updated).build();
Mockito.when(service.findById(Mockito.anyLong())).thenReturn(expected);
RequestBuilder builder = MockMvcRequestBuilders
.get("/memo/id2/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = mockMvc.perform(builder)
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andDo(print())
.andReturn();
MemoView actual = objectMapper.readValue(result.getResponse().getContentAsString(), MemoView.class);
assertThat(actual)
.extracting("title", "description", "done")
.contains(expected.getTitle(), expected.getDescription(), expected.getDone());
}
@Test
public void test_title() throws Exception {
Memo m1 = Memo.builder().id(1L).title("memo1 job").description("memo1 description").done(false).updated(LocalDateTime.now()).build();
Memo m2 = Memo.builder().id(2L).title("memo2 job").description("memo2 description").done(false).updated(LocalDateTime.now()).build();
Memo m3 = Memo.builder().id(3L).title("memo3 job").description("memo3 description").done(false).updated(LocalDateTime.now()).build();
List<Memo> memos = Arrays.asList(m1, m2, m3);
Page<Memo> expected = new PageImpl<>(memos);
Mockito.when(service.findByTitle(Mockito.anyString(), Mockito.any(Pageable.class))).thenReturn(expected);
RequestBuilder builder = MockMvcRequestBuilders
.get("/memo/title/{title}", "job")
.param("page","1")
.param("size", "3")
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = mockMvc.perform(builder)
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$", hasSize(3)))
.andDo(log())
.andDo(print())
.andReturn();
}
}
Since MemoController depends on the AppConfigure class, you need to mock or inject the instance when running unit tests. The above method uses MockBean, but there are other ways to use Import and TestPropertySource.
MemoControllerTests
@RunWith(SpringRunner.class)
@WebMvcTest(value = MemoController.class, secure = false)
@Import(AppConfigure.class)
@TestPropertySource(properties = {
"custom.application.key1=TEST_APP_VALUEA",
"custom.application.key2=TEST_APP_VALUEB"
})
public class MemoControllerTests {
//...abridgement
}
This is a sample implementation of this controller when performing integration testing. Since we do not mock the classes that the controller depends on, we will connect to the database when running the test. This test code assumes that the test data is stored in the table of the database to be connected in advance. Use TestRestTemplate to call the Rest API.
MemoControllerJoinTests
package com.example.application.controller;
import com.example.application.vo.MemoView;
import org.assertj.core.groups.Tuple;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MemoControllerJoinTests {
@Autowired
private TestRestTemplate restTemplate;
private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
@Test
public void test_id() {
MemoView expected = MemoView.builder().title("memo shopping").description("memo1 description").done(false).build();
Map<String, Object> params = new HashMap<>();
params.put("id", 1L);
ResponseEntity<MemoView> actual = restTemplate.getForEntity("/memo/id/{id}", MemoView.class, params);
assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(actual.getHeaders().getContentType()).isEqualTo(contentType);
assertThat(actual.getBody()).isEqualTo(expected);
}
@Test
public void test_id2() {
MemoView expected = MemoView.builder().title("memo shopping").description("memo1 description").done(false).build();
Map<String, Object> params = new HashMap<>();
params.put("id", 1L);
MemoView actual = restTemplate.getForObject("/memo/id2/{id}", MemoView.class, params);
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_title() {
RequestEntity requestEntity = RequestEntity.get(URI.create("/memo/title/job?page=1&size=3&sort=id,desc")).build();
ResponseEntity<List<MemoView>> actual = restTemplate.exchange(requestEntity,
new ParameterizedTypeReference<List<MemoView>>(){});
assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(actual.getHeaders().getContentType()).isEqualTo(contentType);
assertThat(actual.getBody())
.extracting("title", "description", "done")
.containsExactly(
Tuple.tuple("memo job", "memo4 description", false),
Tuple.tuple("memo job", "memo2 description", false)
);
}
}
This is a sample implementation for unit testing view objects. Test that the view object can be converted to the expected JSON object.
MemoViewTests
package com.example.application.vo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import org.springframework.boot.test.json.ObjectContent;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
@RunWith(SpringRunner.class)
@JsonTest
public class MemoViewTests {
@Autowired
private JacksonTester<MemoView> json;
@Test
public void test_serialize() throws IOException {
String expected = "{\"title\":\"memo\",\"description\":\"memo description\",\"completed\":false}";
MemoView memoView = MemoView.builder().title("memo").description("memo description").done(false).build();
JsonContent<MemoView> actual = json.write(memoView);
actual.assertThat().isEqualTo(expected);
actual.assertThat().hasJsonPathStringValue("$.title");
actual.assertThat().extractingJsonPathStringValue("$.title").isEqualTo("memo");
actual.assertThat().hasJsonPathStringValue("$.description");
actual.assertThat().extractingJsonPathStringValue("$.description").isEqualTo("memo description");
actual.assertThat().hasJsonPathBooleanValue("$.completed");
actual.assertThat().extractingJsonPathBooleanValue("$.completed").isEqualTo(false);
}
@Test
public void test_deserialize() throws IOException {
MemoView expected = MemoView.builder().title("memo").description("memo description").done(false).build();
String content = "{\"title\":\"memo\",\"description\":\"memo description\",\"completed\":false}";
ObjectContent<MemoView> actual = json.parse(content);
actual.assertThat().isEqualTo(expected);
}
}
If the build is successful, a file called mmsbs.jar
will be generated under the target directory.
It is assumed that the class that holds the config information referenced by the class implemented in the domain module. Read the settings from the application.yml file.
DomainConfigure
package com.example.domain.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@ConfigurationProperties(prefix = "custom.domain")
@Data
@Slf4j
public class DomainConfigure {
private String key1;
private String key2;
@PostConstruct
public void init() {
log.info("DomainConfigure init : {}", this);
}
}
Since the domain module is in charge of data access processing, there is a class to set the data source. The data source settings refer to application.yml.
DataSourceConfigure
package com.example.domain.datasource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = {"com.example.domain.repository"}
)
public class DataSourceConfigure {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource datasource() {
DataSource dataSource = DataSourceBuilder.create().build();
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder) {
LocalContainerEntityManagerFactoryBean factory = builder
.dataSource(datasource())
.persistenceUnit("default")
.packages("com.example.domain.entity")
.build();
return factory;
}
@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory entityManagerFactory) {
JpaTransactionManager tm = new JpaTransactionManager();
tm.setEntityManagerFactory(entityManagerFactory);
tm.afterPropertiesSet();
return tm;
}
}
Memo
package com.example.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Entity
@Table(name="memo")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Memo implements Serializable {
private static final long serialVersionUID = -7888970423872473471L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="title", nullable = false)
private String title;
@Column(name="description", nullable = false)
private String description;
@Column(name="done", nullable = false)
private Boolean done;
@Column(name="updated", nullable = false)
private LocalDateTime updated;
public static Memo of(String title, String description) {
return Memo.builder()
.title(title)
.description(description)
.done(false)
.updated(LocalDateTime.now())
.build();
}
@PrePersist
private void prePersist() {
done = false;
updated = LocalDateTime.now();
}
@PreUpdate
private void preUpdate() {
updated = LocalDateTime.now();
}
}
CREATE TABLE IF NOT EXISTS memo (
id BIGINT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE NOT NULL,
updated TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL,
PRIMARY KEY (id)
)
ENGINE = INNODB,
CHARACTER SET = utf8mb4,
COLLATE utf8mb4_general_ci;
test data
INSERT INTO memo (id, title, description, done, updated) VALUES
(1, 'memo shopping', 'memo1 description', false, '2017-09-20 12:01:00.123'),
(2, 'memo job', 'memo2 description', false, '2017-09-20 13:02:10.345'),
(3, 'memo private', 'memo3 description', false, '2017-09-20 14:03:21.567'),
(4, 'memo job', 'memo4 description', false, '2017-09-20 15:04:32.789'),
(5, 'memo private', 'memo5 description', false, '2017-09-20 16:05:43.901'),
(6, 'memo travel', 'memo6 description', false, '2017-09-20 17:06:54.234'),
(7, 'memo travel', 'memo7 description', false, '2017-09-20 18:07:05.456'),
(8, 'memo shopping', 'memo8 description', false, '2017-09-20 19:08:16.678'),
(9, 'memo private', 'memo9 description', false, '2017-09-20 20:09:27.890'),
(10,'memo hospital', 'memoA description', false, '2017-09-20 21:10:38.012')
;
MemoRepository
package com.example.domain.repository;
import com.example.domain.entity.Memo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemoRepository extends JpaRepository<Memo, Long> {
Page<Memo> findByTitleLike(String title, Pageable page);
}
interface
MemoService
package com.example.domain.service;
import com.example.domain.entity.Memo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.time.LocalDate;
public interface MemoService {
Memo findById(Long id);
Page<Memo> findByTitle(String title, Pageable page);
Memo registerWeatherMemo(LocalDate date);
}
** Implementation class **
This service depends on a class called WeatherForecast implemented in the common module. Since it is a sample, there is no particular meaning in the implementation content of the service.
MemoServiceImpl
package com.example.domain.service.impl;
import com.example.common.util.WeatherForecast;
import com.example.domain.config.DomainConfigure;
import com.example.domain.entity.Memo;
import com.example.domain.repository.MemoRepository;
import com.example.domain.service.MemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Service
@Slf4j
public class MemoServiceImpl implements MemoService {
@Autowired
private DomainConfigure config;
@Autowired
private MemoRepository memoRepository;
@Autowired
private WeatherForecast weatherForecast;
@Transactional(readOnly = true)
@Override
public Memo findById(Long id) {
log.info("findById - id:{}, config.key1:{}, config.key2:{}", id, config.getKey1(), config.getKey2());
return memoRepository.findOne(id);
}
@Transactional(readOnly = true)
@Override
public Page<Memo> findByTitle(String title, Pageable page) {
log.info("findByTitle - title:{}, page:{}, config.key1:{}, config.key2:{}", title, page, config.getKey1(), config.getKey2());
return memoRepository.findByTitleLike(String.join("","%", title, "%"), page);
}
@Transactional(timeout = 10)
@Override
public Memo registerWeatherMemo(LocalDate date) {
log.info("registerWeatherMemo - date:{}", date);
String title = "weather memo : [" + weatherForecast.getReportDayStringValue(date) + "]";
String description = weatherForecast.report(date);
Memo memo = Memo.builder().title(title).description(description).build();
return memoRepository.saveAndFlush(memo);
}
}
This is an entry point class for the test environment that is required when testing with the domain module. Since some implementation classes depend on the common module, add the common module package to scanBasePackages.
TestApplication
package com.example.domain;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {
"com.example.domain",
"com.example.common"
})
public class TestApplication {
public static void main(String... args) {
SpringApplication.run(TestApplication.class, args);
}
}
Application.yml with configuration information that will be valid in the test environment. Since there are test cases that require a database connection, we will also set the data source.
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/sample_db
username: test_user
password: test_user
driverClassName: com.mysql.jdbc.Driver
jpa:
properties:
hibernate:
show_sql: true
custom:
domain:
key1: test_domain_c
key2: test_domain_d
This is a sample implementation for unit testing a repository. Annotating the test class with DataJpaTest will use in-memory H2 at run time.
MemoRepositoryTests
package com.example.domain.repository;
import com.example.domain.entity.Memo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@DataJpaTest
public class MemoRepositoryTests {
@Autowired
private TestEntityManager entityManager;
@Autowired
private MemoRepository sut;
@BeforeTransaction
public void init() {
//Executed before transaction starts
System.out.println("1. init");
}
@Before
public void setUp() {
//Executed after the transaction starts and before the test method starts
System.out.println("2. setUp");
}
@After
public void tearDown() {
//Executed after the test method ends and before the transaction ends
System.out.println("3. tearDown");
}
@AfterTransaction
public void clear() {
//Executed after the transaction ends
System.out.println("4. clear");
}
@Test
@Sql(statements = {
"INSERT INTO memo (id, title, description, done, updated) VALUES (99999, 'memo test', 'memo description', FALSE, CURRENT_TIMESTAMP)"
})
public void test_findOne() {
Memo expected = entityManager.find(Memo.class, 99999L);
Memo actual = sut.findOne(expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_save() {
Memo expected = Memo.builder().title("memo").description("memo description").build();
sut.saveAndFlush(expected);
entityManager.clear();
Memo actual = entityManager.find(Memo.class, expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_findByTitleLike() {
Memo m1 = Memo.builder().title("memo shopping").description("memo1 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m1);
Memo m2 = Memo.builder().title("memo job").description("memo2 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m2);
Memo m3 = Memo.builder().title("memo private").description("memo3 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m3);
Memo m4 = Memo.builder().title("memo job").description("memo4 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m4);
Memo m5 = Memo.builder().title("memo private").description("memo5 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m5);
entityManager.clear();
List<Memo> expected = Arrays.asList(m4, m2);
Pageable page = new PageRequest(0, 3, Sort.Direction.DESC, "id");
Page<Memo> actual = sut.findByTitleLike("%job%", page);
assertThat(actual.getContent()).isEqualTo(expected);
}
}
This is a sample implementation for repository integration testing. I don't think there are many repository integration tests, but if you want to connect to an external database (MySQL or PostgreSQL) for some reason, this is the implementation.
MemoRepositoryJoinTests
package com.example.domain.repository;
import com.example.domain.entity.Memo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemoRepositoryJoinTests {
@Autowired
private EntityManager entityManager;
@Autowired
private MemoRepository sut;
@Transactional
@Test
@Sql(statements = {
"INSERT INTO memo (id, title, description, done) VALUES (99999, 'memo test', 'memo description', TRUE)"
})
public void test_findOne() {
Memo expected = entityManager.find(Memo.class, 99999L);
Memo actual = sut.findOne(expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Transactional
@Test
public void test_save() {
Memo expected = Memo.builder().title("memo").description("memo description").build();
sut.saveAndFlush(expected);
entityManager.clear();
Memo actual = entityManager.find(Memo.class, expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Transactional
@Test
public void test_findByTitleLike() {
Memo m1 = entityManager.find(Memo.class, 2L);
Memo m2 = entityManager.find(Memo.class, 4L);
List<Memo> expected = Arrays.asList(m2, m1);
Pageable page = new PageRequest(0, 3, Sort.Direction.DESC, "id");
Page<Memo> actual = sut.findByTitleLike("%job%", page);
assertThat(actual.getContent()).isEqualTo(expected);
}
}
It can be attached to a test class or test method. You can prepare test data by executing a sql script file or sql statement.
Note that if you add Sql annotations to both the class and the method, the method level settings will take effect. (It cannot be used to input common data at the class level and method-specific data (difference) at the method level.)
This is a sample implementation for unit testing a service. Dependent classes are mocked. This sample does not use Spring's container function, but uses JUnit, Mockito, and AssertJ.
MemoServiceTests
package com.example.domain.service;
import com.example.common.config.CommonConfigure;
import com.example.common.util.WeatherForecast;
import com.example.domain.config.DomainConfigure;
import com.example.domain.entity.Memo;
import com.example.domain.repository.MemoRepository;
import com.example.domain.service.impl.MemoServiceImpl;
import org.junit.Before;
import org.junit.Test;
import org.mockito.*;
import org.mockito.internal.util.reflection.Whitebox;
import org.springframework.data.domain.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
public class MemoServiceTests {
@Mock
private MemoRepository repository;
@Spy
private WeatherForecast weatherForecast;
@InjectMocks
private MemoServiceImpl sut;
@Before
public void setup(){
MockitoAnnotations.initMocks(this);
DomainConfigure config = new DomainConfigure();
config.setKey1("bean_domain_c");
config.setKey2("bean_domain_d");
Whitebox.setInternalState(sut, "config", config);
CommonConfigure commonConfigure = new CommonConfigure();
commonConfigure.setDatePattern("yyyy-MM-dd");
Whitebox.setInternalState(weatherForecast, "config", commonConfigure);
}
@Test
public void test_findById() {
Memo expected = Memo.builder().id(1L).title("memo").description("memo description").done(false).updated(LocalDateTime.now()).build();
Mockito.when(repository.findOne(Mockito.anyLong())).thenReturn(expected);
Memo actual = sut.findById(expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_findByTitle() {
Memo m1 = Memo.builder().id(2L).title("memo job").description("memo2 description").done(false).updated(LocalDateTime.now()).build();
Memo m2 = Memo.builder().id(4L).title("memo job").description("memo4 description").done(false).updated(LocalDateTime.now()).build();
List<Memo> memos = Arrays.asList(m2, m1);
Page<Memo> expected = new PageImpl<>(memos);
String title = "job";
Pageable page = new PageRequest(0,3, Sort.Direction.DESC, "id");
Mockito.when(repository.findByTitleLike(eq("%" + title + "%"), eq(page))).thenReturn(expected);
Page<Memo> actual = sut.findByTitle(title, page);
assertThat(actual.getContent()).isEqualTo(expected.getContent());
}
@Test
public void test_registerWeatherMemo() {
LocalDate date = LocalDate.of(2017, 9, 20);
Mockito.when(weatherForecast.report(date)).thenReturn("weather forecast : test-test-test-2017-09-20");
Memo expected = Memo.builder().id(1L).title("weather memo :").description("weather forecast : sunny").done(false).updated(LocalDateTime.now()).build();
Mockito.when(repository.saveAndFlush(any(Memo.class))).thenReturn(expected);
Memo actual = sut.registerWeatherMemo(date);
assertThat(actual).isEqualTo(expected);
}
}
In addition to using Whitebox.setInternalState above, you can mock DomainConfigure by using ReflectionTestUtils.
@Before
public void setup(){
MockitoAnnotations.initMocks(this);
DomainConfigure config = new DomainConfigure();
config.setKey1("bean_domain_c");
config.setKey2("bean_domain_d");
ReflectionTestUtils.setField(sut, "config", config);
}
This is a sample implementation for service integration testing. Since the class on which the service to be tested depends is not mocked (some are SpyBean), it also connects to the database. Also, the property values referenced by the WeatherForecast class of the common module that this service depends on must be defined using TestPropertySource.
MemoServiceJoinTests
package com.example.domain.service;
import com.example.common.util.WeatherForecast;
import com.example.domain.TestApplication;
import com.example.domain.datasource.DataSourceConfigure;
import com.example.domain.entity.Memo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = {
TestApplication.class, DataSourceConfigure.class})
@TestPropertySource(properties = {
"custom.common.datePattern=yyyy-MM-dd"
})
public class MemoServiceJoinTests {
@Autowired
private EntityManager entityManager;
@Autowired
private MemoService sut;
@SpyBean
private WeatherForecast weatherForecast;
@Transactional
@Test
public void test_findById() {
Long id = 1L;
Memo expected = entityManager.find(Memo.class, id);
Memo actual = sut.findById(id);
assertThat(actual).isEqualTo(expected);
}
@Transactional
@Test
public void test_findByTitle() {
Memo m1 = entityManager.find(Memo.class, 2L);
Memo m2 = entityManager.find(Memo.class, 4L);
List<Memo> expected = Arrays.asList(m2, m1);
Pageable page = new PageRequest(0,3, Sort.Direction.DESC, "id");
Page<Memo> actual = sut.findByTitle("job", page);
assertThat(actual.getContent()).isEqualTo(expected);
}
@Transactional
@Test
public void test_registerWeatherMemo() {
LocalDate date = LocalDate.of(2017,9,20);
Mockito.when(weatherForecast.report(date)).thenReturn("weather forecast : test-test-test");
Memo actual = sut.registerWeatherMemo(date);
assertThat(actual.getId()).isNotNull();
assertThat(actual.getTitle()).isEqualTo("weather memo : [2017-09-20]");
assertThat(actual.getDescription()).isEqualTo("weather forecast : test-test-test");
}
}
It is assumed that the class that holds the config information referenced by the class implemented in the common module. Read the settings from the application.yml file.
CommonConfigure
package com.example.common.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@ConfigurationProperties(prefix = "custom.common")
@Data
@Slf4j
public class CommonConfigure {
private String key1;
private String key2;
private String datePattern;
@PostConstruct
public void init() {
log.info("CommonConfigure init : {}", this);
}
}
This utility class has no particular meaning because it is an implementation for creating dependencies between modules. For the time being, it is assumed that it is a utility class that calls an external web service to forecast the weather.
WeatherForecast
package com.example.common.util;
import com.example.common.config.CommonConfigure;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component
@Slf4j
public class WeatherForecast {
@Autowired
private CommonConfigure config;
public String getReportDayStringValue(LocalDate reportDay) {
log.debug("getReportDayStringValue - reportDay:{}, config:{}", reportDay, config);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(config.getDatePattern());
return reportDay.format(formatter);
}
public String report(LocalDate reportDay) {
log.debug("report - reportDay:{}", reportDay);
String weather = "weather forecast : " + callForecastApi(reportDay);
return weather;
}
String callForecastApi(LocalDate date) {
// call External Weather API
String apiResult = UUID.randomUUID().toString();
String dateStr = date.toString();
return apiResult + "-" + dateStr;
}
}
This is a sample implementation of this utility class unit test.
WeatherForecastTests
package com.example.common.util;
import com.example.common.config.CommonConfigure;
import org.junit.Before;
import org.junit.Test;
import org.mockito.*;
import org.mockito.internal.util.reflection.Whitebox;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
public class WeatherForecastTests {
@Spy
@InjectMocks
private WeatherForecast sut;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
CommonConfigure config = new CommonConfigure();
config.setKey1("test_common_e");
config.setKey2("test_common_f");
config.setDatePattern("yyyy/MM/dd");
Whitebox.setInternalState(sut, "config", config);
}
@Test
public void test_getReportDayStringValue() {
LocalDate date = LocalDate.of(2017, 9, 20);
String actual = sut.getReportDayStringValue(date);
assertThat(actual).isEqualTo("2017/09/20");
}
@Test
public void test_report() {
LocalDate date = LocalDate.of(2017, 9, 20);
Mockito.when(sut.callForecastApi(date)).thenReturn("test-test-test");
String actual = sut.report(date);
assertThat(actual).isEqualTo("weather forecast : test-test-test");
}
}
In the example of this article, the project is specified for the parent of each module, but if you look at the project of multi module structure on github etc., each module also specifies spring-boot-starter-parent for parent. did.
When comparing instances of entities containing Date type fields as shown below, the assert may fail due to the different string representation of the date.
This is because the implementation class of the instance of the Date type field of the entity returned by Hibernate is of type java.sql.Timestamp
.
@Column(name="updated", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private java.util.Date updated;
org.junit.ComparisonFailure:
Expected :Memo{id=2, title='title', description='desc', done=false, updated=Wed Sep 20 10:34:21 JST 2017}
Actual :Memo{id=2, title='title', description='desc', done=false, updated=2017-09-20 10:34:21.853}
JUnit
annotation | package |
---|---|
RunWith | org.junit.runner.RunWith |
Test | org.junit.Test |
Before | org.junit.Before |
After | org.junit.After |
Spring
annotation | package |
---|---|
SpringBootTest | org.springframework.boot.test.context.SpringBootTest |
ContextConfiguration | org.springframework.test.context.ContextConfiguration |
TestPropertySource | org.springframework.test.context.TestPropertySource |
WebMvcTest | org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest |
DataJpaTest | org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest |
JsonTest | org.springframework.boot.test.autoconfigure.json.JsonTest |
MockBean | org.springframework.boot.test.mock.mockito.MockBean |
SpyBean | org.springframework.boot.test.mock.mockito.SpyBean |
BeforeTransaction | org.springframework.test.context.transaction.BeforeTransaction |
AfterTransaction | org.springframework.test.context.transaction.AfterTransaction |
Sql | org.springframework.test.context.jdbc.Sql |
MockBean and SpyBean are used to Autowired mocked and spy objects.
Mockito
annotation | package |
---|---|
InjectMocks | org.mockito.InjectMocks |
Mock | org.mockito.Mock |
Spy | org.mockito.Spy |
Recommended Posts