Configure Spring Boot application with maven multi module

Overview

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

project

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.

description of pom

There are four pom.xml in total, one for each project and one for each module. The description of each pom is as follows.

Project pom.xml

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>

application module pom.xml

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>

domain module pom.xml

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>

common module pom.xml

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>

Build the 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
}

Description of directory file structure

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

Module description

1. application module

2. Application entry point class

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);
    }
}

3. application.yml file

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

4. application module configuration class

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);
    }
}

5. Controller class to be tested

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());
    }
}

6. View object class

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();
    }
}

7. Unit Test of Controller

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();
    }
}
How to inject AppConfigure

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
}

8. Controller Integration Test

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)
                );
    }
}

9. Unit Test of view object

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);
    }

}
  1. executable jar

If the build is successful, a file called mmsbs.jar will be generated under the target directory.

11. domain module

12. domain module configuration class

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);
    }
}

13. Data source configuration class

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;
    }
}

14. Entity class

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();
    }

}
Table schema
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')
;

15. Repository class to be tested

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);
}

16. Implementation class of the service under test

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);
    }

}

17. Test environment application entry point class

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);
    }
}

18. Test environment application.yml file

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

19. Unit Test of the repository

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);
    }
}

20. Repository Integration Test

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);
    }
}
Sql Annotation

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.)

21. Unit Test of Services

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);
}

22. Integration Test

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");
    }
}

23. common module

24. Common module configuration class

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);
    }
}

25. Utility class to be tested

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;
    }
}

26. Unit Test of utility classes

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");
    }
}

Supplement

Module pom.xml parent

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.

Implementation class for dates returned by Hibernate

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}

Types of annotations used in the test code

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

Configure Spring Boot application with maven multi module
Add module with Spring Boot
Inquiry application creation with Spring Boot
Processing at application startup with Spring Boot
Hello World with Eclipse + Spring Boot + Maven
Start web application development with Spring Boot
Launch Nginx + Spring Boot application with docker-compose
Run WEB application with Spring Boot + Thymeleaf
Spring Boot 2.3 Application Availability
Download with Spring Boot
Include external jar in package with Spring boot2 + Maven3
Try using OpenID Connect with Keycloak (Spring Boot application)
How to boot by environment with Spring Boot of Maven
Generate barcode with Spring Boot
Hello World with Spring Boot
Implement GraphQL with Spring Boot
Get started with Spring boot
Hello World with Spring Boot!
[Spring Boot] Web application creation
Run LIFF with Spring Boot
SNS login with Spring Boot
File upload with Spring Boot
Spring Boot starting with copy
CICS-Run Java application-(4) Spring Boot application
Spring Boot starting with Docker
Hello World with Spring Boot
Set cookies with Spring Boot
Use Spring JDBC with Spring Boot
Getting Started with Spring Boot
Create microservices with Spring Boot
Send email with spring boot
Implement REST API with Spring Boot and JPA (Application Layer)
Spring Boot application that specifies DB connection settings with parameters
Use Basic Authentication with Spring Boot
Spring Boot application development in Eclipse
Spring Boot application code review points
gRPC on Spring Boot with grpc-spring-boot-starter
Create an app with Spring Boot 2
Hot deploy with Spring Boot development
Database linkage with doma2 (Spring boot)
Spring Boot programming with VS Code
Until "Hello World" with Spring Boot
Get validation results with Spring Boot
(Intellij) Hello World with Spring Boot
Create an app with Spring Boot
Google Cloud Platform with Spring Boot 2.0.0
Check date correlation with Spring Boot
Implement Spring Boot application in Gradle
I tried GraphQL with Spring Boot
[Java] LINE integration with Spring Boot
Beginning with Spring Boot 0. Use Spring CLI
I tried Flyway with Spring Boot
Message cooperation started with Spring Boot
Spring Boot gradle build with Docker
Let's make a book management web application with Spring Boot part1
Let's make a book management web application with Spring Boot part3
Let's make a book management web application with Spring Boot part2
[Beginner] Let's write REST API of Todo application with Spring Boot
How to apply thymeleaf changes to the browser immediately with #Spring Boot + maven
Spring Boot2 Web application development with Visual Studio Code SQL Server connection
Send regular notifications with LineNotify + Spring Boot