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
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. |
Insgesamt gibt es vier pom.xml, eine für jedes Projekt und eine für jedes Modul. Die Beschreibung jedes Poms ist wie folgt.
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>
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>
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>
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>
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
}
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
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);
}
}
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
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);
}
}
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());
}
}
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();
}
}
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();
}
}
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
}
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)
);
}
}
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);
}
}
Wenn der Build erfolgreich ist, wird eine Datei mit dem Namen "mmsbs.jar" unter dem Zielverzeichnis generiert.
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);
}
}
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;
}
}
Memo
package com.example.domain.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Entity
@Table(name="memo")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Memo implements Serializable {
private static final long serialVersionUID = -7888970423872473471L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name="title", nullable = false)
private String title;
@Column(name="description", nullable = false)
private String description;
@Column(name="done", nullable = false)
private Boolean done;
@Column(name="updated", nullable = false)
private LocalDateTime updated;
public static Memo of(String title, String description) {
return Memo.builder()
.title(title)
.description(description)
.done(false)
.updated(LocalDateTime.now())
.build();
}
@PrePersist
private void prePersist() {
done = false;
updated = LocalDateTime.now();
}
@PreUpdate
private void preUpdate() {
updated = LocalDateTime.now();
}
}
CREATE TABLE IF NOT EXISTS memo (
id BIGINT NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE NOT NULL,
updated TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL,
PRIMARY KEY (id)
)
ENGINE = INNODB,
CHARACTER SET = utf8mb4,
COLLATE utf8mb4_general_ci;
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')
;
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);
}
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);
}
}
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);
}
}
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
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);
}
}
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);
}
}
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.)
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);
}
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");
}
}
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);
}
}
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;
}
}
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");
}
}
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.
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}
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