[JAVA] Konfigurieren Sie die Spring Boot-Anwendung mit dem Maven Multi-Modul

Überblick

Dies ist ein erklärender Artikel beim Erstellen einer Rest-API-Anwendung mit Spring Boot in einem Projekt mit einer Multimodul-Konfiguration von maven. Die erste Hälfte des Artikels ist eine Beschreibung der logischen (pom description) und physischen (Verzeichnisdateistrukturbeschreibung) Struktur des Multi-Modul-Projekts, und die zweite Hälfte ist eine Ergänzung zu den Eigenschaften jedes Moduls.

Umgebung

Referenz

Projekt

Wir werden die Rest-API-Anwendung verwenden, die aus drei Modulen besteht, wie unten gezeigt, um ein Projekt mit einer Struktur mit mehreren Modulen zu beschreiben.

Modul Root-Paket Erläuterung
application com.example.application Implementierte Kommunikationsverarbeitung mit Clients wie Controllern.
Hängt vom Domänenmodul ab.
domain com.example.domain Implementieren Sie den Datenzugriff (Entitäten und Repositorys) und die Geschäftslogik (Services).
Kommt auf das gemeinsame Modul an.
common com.example.common Implementieren Sie allgemeine Verarbeitungsprogramme wie Dienstprogramme.

Beschreibung von pom

Insgesamt gibt es vier pom.xml, eine für jedes Projekt und eine für jedes Modul. Die Beschreibung jedes Poms ist wie folgt.

Projekt pom.xml

Der POM des übergeordneten Projekts legt die Projektinformationen fest, definiert die Module und definiert die für das gesamte Projekt erforderlichen Abhängigkeiten.

no point
1 Geben Sie Gummi für die Verpackung an
2 Frühling zum Elternteil-boot-starter-Geben Sie das übergeordnete Element an
3 Geben Sie für Module die Module an, aus denen das Projekt besteht.
4 Abhängigkeiten definieren die Abhängigkeiten, die von jedem Modul benötigt werden. Die hier definierte Bibliothek steht allen Modulen zur Verfügung

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>

Anwendungsmodul pom.xml

no point
1 Geben Sie das übergeordnete Projekt im übergeordneten Projekt an
2 Abhängigkeiten definiert die Abhängigkeiten, die vom Anwendungsmodul benötigt werden
3 Die Build-Einstellungen werden im Abschnitt des Anwendungsmoduls beschrieben

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>

Domänenmodul pom.xml

no point
1 Geben Sie das übergeordnete Projekt im übergeordneten Projekt an
2 Abhängigkeiten definiert die Abhängigkeiten, die vom Domänenmodul benötigt werden

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>

gemeinsames Modul pom.xml

no point
1 Geben Sie das übergeordnete Projekt im übergeordneten Projekt an
2 Abhängigkeiten definieren die Abhängigkeiten, die vom allgemeinen Modul benötigt werden, sind jedoch nicht angegeben, da bisher nichts benötigt wird.

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>

Erstellen Sie das Projekt

Führen Sie den folgenden Befehl mvn im zu erstellenden Projektverzeichnis aus.

> mvn clean package

Fügen Sie die folgenden Optionen hinzu, um den Test zu überspringen.

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

Die Artefakte werden im Zielverzeichnis jedes Moduls ausgegeben. Das Artefakt (ausführbare JAR) als Spring Boot-Anwendung wird im Zielverzeichnis des Anwendungsmoduls erstellt. Führen Sie den folgenden Befehl aus, um es auszuführen.

> java -jar application\target\mmsbs.jar

...Kürzung...

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

** Beispiel für die Ausführung der Rest-API **

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

Beschreibung der Verzeichnisdateistruktur

Das physische Verzeichnis und die Dateistruktur des Projekts sind wie folgt. Das Projektverzeichnis ist eine einfache Struktur, in der pom.xml für das Projekt und das Verzeichnis jedes Moduls gespeichert wird. Wenn Sie Ihr Projekt mit git verwalten, befindet sich auch das Repository-Verzeichnis .git hier. Das Verzeichnis jedes Moduls hat die Struktur eines normalen Maven-Projekts.

/mmsbs
  |
  +--- /.git
  +--- pom.xml
  +--- README.md
  |
  +--- /application                                                  <---(1)
  |      |
  |      +--- pom.xml
  |      |
  |      +--- /src
  |      |      |
  |      |      +--- /main
  |      |      |      |
  |      |      |      +--- /java
  |      |      |      |      |
  |      |      |      |      +--- /com.example
  |      |      |      |             |
  |      |      |      |             +--- Application.java           <---(2)Anwendungseinstiegspunkt
  |      |      |      |             +--- WebMvcConfigure.java
  |      |      |      |             |
  |      |      |      |             +--- /application
  |      |      |      |                    |
  |      |      |      |                    +--- /config             <---(4)
  |      |      |      |                    +--- /controller         <---(5)
  |      |      |      |                    +--- /interceptor
  |      |      |      |                    +--- /vo                 <---(6)
  |      |      |      |
  |      |      |      +--- /resources
  |      |      |             |
  |      |      |             +--- application.yml                   <---(3)Anwendungskonfigurationsdatei
  |      |      |             +--- 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

Modulbeschreibung

1. Anwendungsmodul

2. Anwendungseinstiegspunktklasse

In der Konfiguration mit mehreren Modulen gibt es keine spezielle Implementierung. Es handelt sich um eine Einstiegspunktklasse für normale Spring Boot-Anwendungen.

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

Zusätzlich zu den Spring Boot-Einstellungen werden auch die für jedes Modul spezifischen Einstellungen beschrieben. In Anbetracht der Unabhängigkeit des Moduls ist es besser, die für das Modul spezifischen Einstellwerte im Modul zu platzieren. Die Priorisierung der Bequemlichkeit erfolgt jedoch in application.yml. Da es sich um ein Beispiel handelt, hat der Einstellwert keine besondere Bedeutung.

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. Konfigurationsklasse des Anwendungsmoduls

Es wird davon ausgegangen, dass die Klasse die Konfigurationsinformationen enthält, auf die von der im Anwendungsmodul implementierten Klasse verwiesen wird. Lesen Sie die Einstellungen aus der Datei 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. Zu testende Controller-Klasse

Es ist eine einfache API, die daraufhin die Daten der Memo-Tabelle verarbeitet (zu einem Ansichtsobjekt verarbeitet). Der Controller hängt von dem im Domänenmodul implementierten MemoService und der AppConfigure-Klasse im Anwendungsmodul ab. Die Handler-Methode mit dem Namen id2 ist übrigens eine Version, die den Rückgabewert der Handler-Methode mit dem Namen id ändert (ohne ResponseEntity zu verwenden).

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

    //Muster, das ResponseEntity nicht verwendet
    @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. Objektklasse anzeigen

Es wird angenommen, dass diese Klasse ein Ansichtsobjekt ist, das Informationen enthält, die auf den Client reagieren. In dieser Anwendung wird die Entität nicht wie sie ist zurückgegeben, sondern einmal in ein Ansichtsobjekt konvertiert.

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 der Steuerung

Dies ist ein Beispiel, das so implementiert wird, wenn ein Komponententest dieses Controllers durchgeführt wird. Klassen, von denen der Controller abhängt, werden mit der MockBean-Annotation verspottet. Außerdem wird keine Verbindung zur Datenbank hergestellt, wenn der Test ausgeführt wird.

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();
    }
}
So injizieren Sie AppConfigure

Da MemoController von der AppConfigure-Klasse abhängt, müssen Sie die Instanz verspotten oder injizieren, wenn Sie einen Komponententest ausführen. Die obige Methode verwendet MockBean, es gibt jedoch auch andere Möglichkeiten, Import und TestPropertySource zu verwenden.

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 {
  //...Kürzung
}

8. Controller-Integrationstest

Dies ist eine Beispielimplementierung, wenn ein Integrationstest dieses Controllers durchgeführt wird. Da wir die Klassen, von denen der Controller abhängt, nicht verspotten, stellen wir beim Ausführen des Tests eine Verbindung zur Datenbank her. Dieser Testcode setzt voraus, dass die Testdaten vorab in der Tabelle der verbundenen Datenbank gespeichert sind. Verwenden Sie TestRestTemplate, um die Rest-API aufzurufen.

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 des Ansichtsobjekts

Dies ist eine Beispielimplementierung zum Testen von Ansichtsobjekten. Testen Sie, ob das Ansichtsobjekt in das erwartete JSON-Objekt konvertiert werden kann.

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

Wenn der Build erfolgreich ist, wird eine Datei mit dem Namen "mmsbs.jar" unter dem Zielverzeichnis generiert.

11. Domänenmodul

12. Konfigurationsklasse des Domänenmoduls

Es wird davon ausgegangen, dass die Klasse die Konfigurationsinformationen enthält, auf die von der im Domänenmodul implementierten Klasse verwiesen wird. Lesen Sie die Einstellungen aus der Datei 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. Datenquellen-Konfigurationsklasse

Da das Domänenmodul für die Datenzugriffsverarbeitung zuständig ist, gibt es eine Klasse, die die Datenquelle festlegt. Die Datenquelleneinstellungen beziehen sich auf 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. Entitätsklasse

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

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

Testdaten

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. Zu testende Repository-Klasse

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. Implementierungsklasse des zu testenden Dienstes

Schnittstelle

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

** Implementierungsklasse **

Dieser Dienst hängt von einer Klasse namens WeatherForecast ab, die im allgemeinen Modul implementiert ist. Da es sich um ein Beispiel handelt, haben die Implementierungsinhalte des Dienstes keine besondere Bedeutung.

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. Einstiegspunktklasse der Anwendung in der Testumgebung

Dies ist eine Einstiegspunktklasse für die Testumgebung, die beim Testen mit dem Domänenmodul erforderlich ist. Da einige Implementierungsklassen vom allgemeinen Modul abhängen, fügen Sie das allgemeine Modulpaket zu scanBasePackages hinzu.

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. Testumgebung application.yml-Datei

Application.yml mit Konfigurationsinformationen, die in der Testumgebung gültig sind. Da es Testfälle gibt, für die eine Datenbankverbindung erforderlich ist, legen wir auch die Datenquelle fest.

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 des Repositorys

Dies ist eine Beispielimplementierung für Unit-Tests eines Repositorys. Durch Hinzufügen der DataJpaTest-Annotation zur Testklasse wird zur Laufzeit In-Memory H2 verwendet.

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() {
        //Wird ausgeführt, bevor die Transaktion beginnt
        System.out.println("1. init");
    }
    @Before
    public void setUp() {
        //Wird ausgeführt, nachdem die Transaktion gestartet wurde und bevor die Testmethode gestartet wurde
        System.out.println("2. setUp");
    }
    @After
    public void tearDown() {
        //Wird ausgeführt, nachdem die Testmethode beendet wurde und bevor die Transaktion endet
        System.out.println("3. tearDown");
    }
    @AfterTransaction
    public void clear() {
        //Wird ausgeführt, nachdem die Transaktion beendet wurde
        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. Integrationstest

Dies ist eine Beispielimplementierung für Repository-Integrationstests. Ich glaube nicht, dass das Repository viele Integrationstests durchführt, aber wenn Sie aus irgendeinem Grund eine Verbindung zu einer externen Datenbank (MySQL oder PostgreSQL) herstellen möchten, ist dies meiner Meinung nach die Implementierung.

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 Anmerkung

Es kann an eine Testklasse oder Testmethode angehängt werden. Sie können Testdaten vorbereiten, indem Sie eine SQL-Skriptdatei oder eine SQL-Anweisung ausführen.

Beachten Sie, dass die Einstellungen auf Methodenebene wirksam werden, wenn Sie der Klasse und der Methode die SQL-Annotation hinzufügen. (Es kann nicht verwendet werden, um allgemeine Daten auf Klassenebene und methodenspezifische Daten (Differenz) auf Methodenebene einzugeben.)

21. Unit Test of Service

Dies ist ein Beispiel für eine solche Implementierung, wenn ein Komponententest eines Dienstes durchgeführt wird. Abhängige Klassen werden verspottet. Dieses Beispiel verwendet nicht die Containerfunktion von Spring, sondern JUnit, Mockito und 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);
    }
}

Zusätzlich zur Verwendung von Whitebox.setInternalState oben können Sie DomainConfigure mithilfe von ReflectionTestUtils verspotten.

@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. Integrationstest

Dies ist eine Beispielimplementierung für Service-Integrationstests. Klassen, von denen der zu testende Dienst abhängt, werden nicht verspottet (einige sind SpyBean), daher stellen sie auch eine Verbindung zur Datenbank her. Außerdem müssen die Eigenschaftswerte, auf die die WeatherForecast-Klasse des allgemeinen Moduls verweist, von dem dieser Service abhängt, mit TestPropertySource definiert werden.

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. gemeinsames Modul

24. Allgemeine Modulkonfigurationsklasse

Es wird angenommen, dass die Klasse, die die Konfigurationsinformationen enthält, auf die von der im allgemeinen Modul implementierten Klasse verwiesen wird. Lesen Sie die Einstellungen aus der Datei 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. Zu testende Gebrauchsklasse

Diese Dienstprogrammklasse hat keine besondere Bedeutung, da sie eine Implementierung zum Erstellen von Abhängigkeiten zwischen Modulen darstellt. Derzeit wird davon ausgegangen, dass es sich um eine Dienstprogrammklasse handelt, die einen externen Webdienst aufruft, um Wettervorhersagen durchzuführen.

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 der Gebrauchsklasse

Dies ist ein Beispiel, das wie folgt implementiert wird, wenn ein Komponententest dieser Dienstprogrammklasse durchgeführt wird.

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

Ergänzung

Übergeordnetes Modul pom.xml

Im Beispiel dieses Artikels wird das Projekt für das übergeordnete Element jedes Moduls angegeben. Wenn Sie sich jedoch das Projekt mit mehreren Modulstrukturen auf Github usw. ansehen, gibt jedes Modul auch das übergeordnete Spring-Boot-Starter-Parent für das übergeordnete Modul an. tat.

Implementierungsklasse für von Hibernate zurückgegebene Daten

Beim Vergleichen von Instanzen einer Entität, die ein Feld vom Typ Datum enthält (siehe unten), kann die Bestätigung aufgrund der unterschiedlichen Zeichenfolgendarstellung des Datums fehlschlagen. Dies liegt daran, dass die Implementierungsklasse der Instanz des Datumsfelds der von Hibernate zurückgegebenen Entität vom Typ "java.sql.Timestamp" ist.

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

Arten von Anmerkungen, die im Testcode verwendet werden

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 und SpyBean werden verwendet, um verspottete und ausspionierte Objekte automatisch zu verdrahten.

Mockito

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

Recommended Posts

Konfigurieren Sie die Spring Boot-Anwendung mit dem Maven Multi-Modul
Modul mit Spring Boot hinzufügen
Erstellen Sie eine Anfrage-App mit Spring Boot
Verarbeitung beim Starten einer Anwendung mit Spring Boot
Hallo Welt mit Eclipse + Spring Boot + Maven
Starten Sie die Entwicklung von Webanwendungen mit Spring Boot
Starten Sie die Nginx + Spring Boot-Anwendung mit Docker-Compose
Führen Sie die WEB-Anwendung mit Spring Boot + Thymeleaf aus
Spring Boot 2.3 Verfügbarkeit von Anwendungen
Mit Spring Boot herunterladen
Nehmen Sie das externe Glas mit Spring boot2 + Maven3 in das Paket auf
Versuchen Sie, OpenID Connect mit Keycloak (Spring Boot-Anwendung) zu verwenden.
Booten nach Umgebung mit Spring Boot of Maven
Generieren Sie mit Spring Boot einen Barcode
Hallo Welt mit Spring Boot
Implementieren Sie GraphQL mit Spring Boot
Beginnen Sie mit Spring Boot
Hallo Welt mit Spring Boot!
[Spring Boot] Erstellung von Webanwendungen
Führen Sie LIFF mit Spring Boot aus
SNS-Login mit Spring Boot
Datei-Upload mit Spring Boot
Spring Boot beginnt mit dem Kopieren
CICS-Java-Anwendung ausführen- (4) Spring Boot-App
Spring Boot beginnend mit Docker
Hallo Welt mit Spring Boot
Setzen Sie Cookies mit Spring Boot
Verwenden Sie Spring JDBC mit Spring Boot
Erste Schritte mit Spring Boot
Erstellen Sie mit Spring Boot einen Mikrodienst
Mail mit Spring Boot verschicken
Implementieren Sie die REST-API mit Spring Boot und JPA (Application Layer).
Spring Boot-Anwendung, die DB-Verbindungseinstellungen mit Parametern angibt
Verwenden Sie die Standardauthentifizierung mit Spring Boot
Spring Boot-Anwendungsentwicklung in Eclipse
Überprüfungspunkte für den Spring Boot-Anwendungscode
gRPC auf Spring Boot mit grpc-spring-boot-Starter
Erstellen Sie eine App mit Spring Boot 2
Hot Deploy mit Spring Boot-Entwicklung
Datenbankverknüpfung mit doma2 (Spring Boot)
Spring Boot Programmierung mit VS Code
Bis "Hallo Welt" mit Spring Boot
Erhalten Sie Validierungsergebnisse mit Spring Boot
(Intellij) Hallo Welt mit Spring Boot
Erstellen Sie eine App mit Spring Boot
Google Cloud Platform mit Spring Boot 2.0.0
Implementieren Sie die Spring Boot-Anwendung in Gradle
Ich habe GraphQL mit Spring Boot ausprobiert
[Java] LINE-Integration mit Spring Boot
Beginnend mit Spring Boot 0. Verwenden Sie Spring CLI
Ich habe Flyway mit Spring Boot ausprobiert
Die Nachrichtenkooperation begann mit Spring Boot
Erstellen wir eine Buchverwaltungs-Webanwendung mit Spring Boot part1
Lassen Sie uns mit Spring Boot part3 eine Webanwendung für die Buchverwaltung erstellen
Lassen Sie uns mit Spring Boot part2 eine Webanwendung für die Buchverwaltung erstellen
[Anfänger] Versuchen Sie, die REST-API für die Todo-App mit Spring Boot zu schreiben
So wenden Sie Thymeleaf-Änderungen sofort mit #Spring Boot + maven auf den Browser an
Spring Boot2-Webanwendungsentwicklung mit Visual Studio Code SQL Server-Verbindung
Senden Sie regelmäßige Benachrichtigungen mit LineNotify + Spring Boot