[JAVA] Configurer l'application Spring Boot avec le module multiple maven

Aperçu

Ceci est un article explicatif lors de la création d'une application API Rest à l'aide de Spring Boot dans un projet avec une configuration multi-module de maven. La première moitié de l'article est une description de la structure logique (description pom) et physique (description de la structure du fichier répertoire) du projet multi-module, et la seconde moitié est un complément aux fonctionnalités de chaque module.

environnement

référence

projet

Nous utiliserons l'application Rest API, qui se compose de trois modules, comme indiqué ci-dessous pour décrire un projet avec une structure multi-modules.

module Paquet racine La description
application com.example.application Implémentation du traitement des communications avec les clients tels que les contrôleurs.
Dépend du module de domaine.
domain com.example.domain Mettre en œuvre l'accès aux données (entités et référentiels) et la logique métier (services).
Dépend du module commun.
common com.example.common Implémentez des traitements courants tels que des utilitaires.

description de pom

Il y a quatre pom.xml au total, un pour chaque projet et un pour chaque module. La description de chaque pom est la suivante.

Projet pom.xml

Le pom du projet parent définit les informations du projet, définit les modules et définit les dépendances requises pour l'ensemble du projet.

no point
1 Spécifier le caoutchouc pour l'emballage
2 ressort aux parents-boot-starter-Spécifier le parent
3 Pour les modules, spécifiez les modules qui composent le projet.
4 Les dépendances définissent les dépendances requises par chaque module. La bibliothèque définie ici est disponible pour tous les 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>

module d'application pom.xml

no point
1 Spécifiez le projet parent dans le parent
2 dependencies définit les dépendances requises par le module d'application
3 Les paramètres de construction sont décrits dans le pom du module d'application

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>

module de domaine pom.xml

no point
1 Spécifiez le projet parent dans le parent
2 dependencies définit les dépendances requises par le module de domaine

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>

module commun pom.xml

no point
1 Spécifiez le projet parent dans le parent
2 Les dépendances définissent les dépendances requises par le module commun, mais ne sont pas spécifiées car il n'y a rien de nécessaire jusqu'à présent.

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>

Construisez le projet

Exécutez la commande mvn suivante dans le répertoire du projet à générer.

> mvn clean package

Ajoutez les options suivantes pour ignorer le test.

> mvn clean package -Dmaven.test.skip=true

Les artefacts sont sortis sous le répertoire cible de chaque module. L'artefact (jar exécutable) en tant qu'application Spring Boot est créé dans le répertoire cible du module d'application. Pour l'exécuter, exécutez la commande suivante.

> java -jar application\target\mmsbs.jar

...réduction...

Tomcat started on port(s): 8080 (http)
Started Application in 14.52 seconds (JVM running for 15.502)

** Exemple d'exécution d'API Rest **

> 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 de la structure des fichiers de répertoire

Le répertoire physique et la structure des fichiers du projet sont les suivants. Le répertoire du projet est une structure simple qui stocke pom.xml pour le projet et le répertoire de chaque module. Si vous gérez votre projet avec git, le répertoire du référentiel .git sera également ici. Le répertoire de chaque module a la structure d'un projet maven normal.

/mmsbs
  |
  +--- /.git
  +--- pom.xml
  +--- README.md
  |
  +--- /application                                                  <---(1)
  |      |
  |      +--- pom.xml
  |      |
  |      +--- /src
  |      |      |
  |      |      +--- /main
  |      |      |      |
  |      |      |      +--- /java
  |      |      |      |      |
  |      |      |      |      +--- /com.example
  |      |      |      |             |
  |      |      |      |             +--- Application.java           <---(2)Point d'entrée de l'application
  |      |      |      |             +--- WebMvcConfigure.java
  |      |      |      |             |
  |      |      |      |             +--- /application
  |      |      |      |                    |
  |      |      |      |                    +--- /config             <---(4)
  |      |      |      |                    +--- /controller         <---(5)
  |      |      |      |                    +--- /interceptor
  |      |      |      |                    +--- /vo                 <---(6)
  |      |      |      |
  |      |      |      +--- /resources
  |      |      |             |
  |      |      |             +--- application.yml                   <---(3)Fichier de configuration de l'application
  |      |      |             +--- 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

Description du module

1. module d'application

2. Classe de point d'entrée de l'application

Il n'y a pas d'implémentation spéciale dans la configuration multi-modules, c'est une classe de point d'entrée pour les applications Spring Boot ordinaires.

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. fichier application.yml

En plus des paramètres Spring Boot, les paramètres spécifiques à chaque module sont également décrits. Compte tenu de l'indépendance du module, il est préférable de placer les valeurs de paramétrage spécifiques au module dans le module, mais en privilégiant la commodité, elles sont résumées dans application.yml. Puisqu'il s'agit d'un échantillon, la valeur de réglage n'a pas de signification particulière.

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. classe de configuration du module d'application

On suppose que la classe qui contient les informations de configuration référencées par la classe implémentée dans le module d'application. Lisez les paramètres du fichier application.yml.

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. Classe de contrôleur à tester

C'est une API simple qui répond en traitant les données de la table Memo (transformées en un objet de vue). Le contrôleur dépend du MemoService implémenté dans le module de domaine et de la classe AppConfigure dans le module d'application. À propos, la méthode de gestionnaire appelée id2 est une version qui modifie la valeur de retour de la méthode de gestionnaire appelée id (sans utiliser 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);
    }

    //Modèle qui n'utilise pas 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. Afficher la classe d'objets

Cette classe est supposée être un objet de vue contenant des informations qui répondent au client. Dans cette application, l'entité n'est pas retournée telle quelle, mais est convertie une fois en objet de vue.

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. Test unitaire du contrôleur

Ceci est un exemple qui sera implémenté comme ceci lors de l'exécution d'un test unitaire de ce contrôleur. Les classes dont dépend le contrôleur sont simulées avec l'annotation MockBean. De plus, il ne se connecte pas à la base de données lorsque le test est exécuté.

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

Étant donné que MemoController dépend de la classe AppConfigure, vous devez simuler ou injecter l'instance lors de l'exécution d'un test unitaire. La méthode ci-dessus utilise MockBean, mais il existe d'autres façons d'utiliser Import et 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 {
  //...réduction
}

8. Test d'intégration du contrôleur

Il s'agit d'un exemple d'implémentation lors de l'exécution d'un test d'intégration de ce contrôleur. Il ne se moque pas des classes dont dépend le contrôleur, il se connecte donc à la base de données lors de l'exécution du test. Ce code de test suppose que les données de test sont stockées à l'avance dans la table de la base de données connectée. Utilisez TestRestTemplate pour appeler l'API Rest.

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. Test unitaire de l'objet de vue

Il s'agit d'un exemple d'implémentation pour le test unitaire des objets de vue. Vérifiez que l'objet de vue peut être converti en objet JSON attendu.

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

Si la construction réussit, un fichier appelé mmsbs.jar sera généré sous le répertoire cible.

11. module de domaine

12. classe de configuration du module de domaine

On suppose que la classe qui contient les informations de configuration référencées par la classe implémentée dans le module de domaine. Lisez les paramètres du fichier application.yml.

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. Classe de configuration de la source de données

Étant donné que le module de domaine est en charge du traitement de l'accès aux données, il existe une classe qui définit la source de données. Les paramètres de la source de données font référence à 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. Classe d'entité

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

}
Schéma de table
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;

données de test

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. Classe de référentiel à tester

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. Classe d'implémentation du service 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);
}

** Classe d'implémentation **

Ce service dépend d'une classe appelée WeatherForecast implémentée dans le module commun. Puisqu'il s'agit d'un échantillon, il n'y a pas de signification particulière dans le contenu d'implémentation du 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. Classe d'application du point d'entrée dans l'environnement de test

Il s'agit d'une classe de point d'entrée pour l'environnement de test qui est requise lors des tests avec le module de domaine. Étant donné que certaines classes d'implémentation dépendent du module commun, ajoutez le package de module commun à 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. Fichier application.yml d'environnement de test

Application.yml avec les informations de configuration qui seront valides dans l'environnement de test. Étant donné que certains cas de test nécessitent une connexion à la base de données, nous définissons également la source de données.

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. Test unitaire du référentiel

Il s'agit d'un exemple d'implémentation pour les tests unitaires d'un référentiel. L'ajout de l'annotation DataJpaTest à la classe de test utilisera H2 en mémoire au moment de l'exécution.

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() {
        //Exécuté avant le début de la transaction
        System.out.println("1. init");
    }
    @Before
    public void setUp() {
        //Exécuté après le début de la transaction et avant le début de la méthode de test
        System.out.println("2. setUp");
    }
    @After
    public void tearDown() {
        //Exécuté après la fin de la méthode de test et avant la fin de la transaction
        System.out.println("3. tearDown");
    }
    @AfterTransaction
    public void clear() {
        //Exécuté après la fin de la transaction
        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. Test d'intégration

Il s'agit d'un exemple d'implémentation pour les tests d'intégration de référentiel. Je ne pense pas qu'il y ait beaucoup de tests d'intégration de référentiel, mais si vous voulez vous connecter à une base de données externe (MySQL ou PostgreSQL) pour une raison quelconque, c'est l'implémentation.

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

Il peut être associé à une classe de test ou à une méthode de test. Vous pouvez préparer les données de test en exécutant un fichier de script SQL ou une instruction SQL.

Notez que si vous ajoutez l'annotation SQL à la fois à la classe et à la méthode, les paramètres au niveau de la méthode prendront effet. (Il ne peut pas être utilisé pour saisir des données communes au niveau de la classe et des données spécifiques à la méthode (différence) au niveau de la méthode.)

21. Test de service unitaire

Voici un exemple d'une telle implémentation lors de l'exécution d'un test unitaire d'un service. Les classes dépendantes sont ridiculisées. Cet exemple n'utilise pas la fonction de conteneur de Spring, mais utilise JUnit, Mockito et 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);
    }
}

En plus d'utiliser Whitebox.setInternalState ci-dessus, vous pouvez simuler DomainConfigure à l'aide de 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. Test d'intégration

Il s'agit d'un exemple d'implémentation pour les tests d'intégration de services. Les classes dont dépend le service à tester ne sont pas simulées (certaines sont SpyBean), elles se connectent donc également à la base de données. En outre, les valeurs de propriété référencées par la classe WeatherForecast du module commun dont dépend ce service doivent être définies à l'aide de 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. module commun

24. Classe de configuration de module commune

On suppose que la classe qui contient les informations de configuration référencées par la classe implémentée dans le module commun. Lisez les paramètres du fichier application.yml.

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. Classe d'utilité à tester

Cette classe utilitaire n'a pas de signification particulière car il s'agit d'une implémentation pour créer des dépendances entre les modules. Pour le moment, on suppose qu'il s'agit d'une classe utilitaire qui appelle un service Web externe pour effectuer des prévisions météorologiques.

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. Test unitaire de la classe d'utilité

Ceci est un exemple qui sera implémenté comme ceci lors de l'exécution d'un test unitaire de cette classe d'utilitaire.

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

Supplément

Parent du module pom.xml

Dans l'exemple de cet article, le projet est spécifié pour le parent de chaque module, mais si vous regardez le projet de structure multi module sur github, etc., chaque module spécifie également spring-boot-starter-parent pour le parent. fait.

Classe d'implémentation pour les dates renvoyées par Hibernate

Lors de la comparaison des instances d'une entité qui contient un champ de type Date comme indiqué ci-dessous, l'assertion peut échouer car la représentation sous forme de chaîne de la date est différente. En effet, la classe d'implémentation de l'instance du champ de type Date de l'entité renvoyée par Hibernate est de 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 d'annotations utilisées dans le code de test

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 et SpyBean sont utilisés pour les objets moqués et espionnés Autowired.

Mockito

annotation package
InjectMocks org.mockito.InjectMocks
Mock org.mockito.Mock
Spy org.mockito.Spy

Recommended Posts

Configurer l'application Spring Boot avec le module multiple maven
Ajouter un module avec Spring Boot
Créer une application d'enquête avec Spring Boot
Traitement lors du démarrage d'une application avec Spring Boot
Hello World avec Eclipse + Spring Boot + Maven
Démarrez le développement d'applications Web avec Spring Boot
Lancez l'application Nginx + Spring Boot avec docker-compose
Exécutez l'application WEB avec Spring Boot + Thymeleaf
Disponibilité de l'application Spring Boot 2.3
Télécharger avec Spring Boot
Inclure le pot externe dans le package avec Spring boot2 + Maven3
Essayez d'utiliser OpenID Connect avec Keycloak (application Spring Boot)
Comment démarrer par environnement avec Spring Boot de Maven
Générer un code à barres avec Spring Boot
Hello World avec Spring Boot
Implémenter GraphQL avec Spring Boot
Démarrez avec Spring Boot
Bonjour tout le monde avec Spring Boot!
[Spring Boot] Création d'applications Web
Exécutez LIFF avec Spring Boot
Connexion SNS avec Spring Boot
Téléchargement de fichiers avec Spring Boot
Spring Boot commençant par copie
Application Java CICS-Run - (4) Application Spring Boot
Spring Boot à partir de Docker
Hello World avec Spring Boot
Définir des cookies avec Spring Boot
Utiliser Spring JDBC avec Spring Boot
Premiers pas avec Spring Boot
Créer un micro service avec Spring Boot
Envoyer du courrier avec Spring Boot
Implémenter l'API REST avec Spring Boot et JPA (Application Layer)
Application Spring Boot qui spécifie les paramètres de connexion à la base de données avec des paramètres
Utiliser l'authentification de base avec Spring Boot
Développement d'applications Spring Boot dans Eclipse
Points de révision du code de l'application Spring Boot
gRPC sur Spring Boot avec grpc-spring-boot-starter
Créez une application avec Spring Boot 2
Déploiement à chaud avec le développement Spring Boot
Liaison de base de données avec doma2 (Spring boot)
Programmation Spring Boot avec VS Code
Jusqu'à "Hello World" avec Spring Boot
Obtenez des résultats de validation avec Spring Boot
(Intellij) Hello World avec Spring Boot
Créez une application avec Spring Boot
Google Cloud Platform avec Spring Boot 2.0.0
Implémenter l'application Spring Boot dans Gradle
J'ai essayé GraphQL avec Spring Boot
[Java] Intégration LINE avec Spring Boot
À partir de Spring Boot 0. Utilisez Spring CLI
J'ai essayé Flyway avec Spring Boot
La coopération des messages a commencé avec Spring Boot
Créons une application Web de gestion de livres avec Spring Boot part1
Créons une application Web de gestion de livres avec Spring Boot part3
Créons une application Web de gestion de livres avec Spring Boot part2
[Débutant] Essayez d'écrire l'API REST pour l'application Todo avec Spring Boot
Comment appliquer immédiatement les modifications de Thymeleaf au navigateur avec #Spring Boot + maven
Développement d'applications Web Spring Boot2 avec connexion Visual Studio Code SQL Server
Envoyez des notifications régulières avec LineNotify + Spring Boot