Ceci est un article explicatif lors de la création d'une application API Rest à l'aide de Spring Boot dans un projet avec une configuration multi-module de maven. La première moitié de l'article est une description de la structure logique (description pom) et physique (description de la structure du fichier répertoire) du projet multi-module, et la seconde moitié est un complément aux fonctionnalités de chaque module.
environnement
référence
Nous utiliserons l'application Rest API, qui se compose de trois modules, comme indiqué ci-dessous pour décrire un projet avec une structure multi-modules.
module | Paquet racine | La description |
---|---|---|
application | com.example.application | Implémentation du traitement des communications avec les clients tels que les contrôleurs. Dépend du module de domaine. |
domain | com.example.domain | Mettre en œuvre l'accès aux données (entités et référentiels) et la logique métier (services). Dépend du module commun. |
common | com.example.common | Implémentez des traitements courants tels que des utilitaires. |
Il y a quatre pom.xml au total, un pour chaque projet et un pour chaque module. La description de chaque pom est la suivante.
Le pom du projet parent définit les informations du projet, définit les modules et définit les dépendances requises pour l'ensemble du projet.
no | point |
---|---|
1 | Spécifier le caoutchouc pour l'emballage |
2 | ressort aux parents-boot-starter-Spécifier le parent |
3 | Pour les modules, spécifiez les modules qui composent le projet. |
4 | Les dépendances définissent les dépendances requises par chaque module. La bibliothèque définie ici est disponible pour tous les modules |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- point.1 -->
<packaging>pom</packaging>
<name>mmsbs</name>
<description>Multi Modules Spring Boot Sample application</description>
<!-- point.2 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/>
</parent>
<!-- point.3 -->
<modules>
<module>application</module>
<module>domain</module>
<module>common</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<!-- point.4 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
no | point |
---|---|
1 | Spécifiez le projet parent dans le parent |
2 | dependencies définit les dépendances requises par le module d'application |
3 | Les paramètres de construction sont décrits dans le pom du module d'application |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>application</name>
<description>Application Module</description>
<!-- point.1 -->
<parent>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- point.2 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<finalName>mmsbs</finalName>
<plugins>
<!-- point.3 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludeDevtools>true</excludeDevtools>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<compilerVersion>1.8</compilerVersion>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<!--<arg>-verbose</arg>-->
<arg>-Xlint:all,-options,-path</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
no | point |
---|---|
1 | Spécifiez le projet parent dans le parent |
2 | dependencies définit les dépendances requises par le module de domaine |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>domain</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>domain</name>
<description>Domain Module</description>
<!-- point.1 -->
<parent>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- point.2 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-java8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
no | point |
---|---|
1 | Spécifiez le projet parent dans le parent |
2 | Les dépendances définissent les dépendances requises par le module commun, mais ne sont pas spécifiées car il n'y a rien de nécessaire jusqu'à présent. |
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>common</name>
<description>Common Module</description>
<!-- point.1 -->
<parent>
<groupId>com.example</groupId>
<artifactId>mmsbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<!-- point.2 -->
<dependency>
</dependency>
</project>
Exécutez la commande mvn suivante dans le répertoire du projet à générer.
> mvn clean package
Ajoutez les options suivantes pour ignorer le test.
> mvn clean package -Dmaven.test.skip=true
Les artefacts sont sortis sous le répertoire cible de chaque module. L'artefact (jar exécutable) en tant qu'application Spring Boot est créé dans le répertoire cible du module d'application. Pour l'exécuter, exécutez la commande suivante.
> java -jar application\target\mmsbs.jar
...réduction...
Tomcat started on port(s): 8080 (http)
Started Application in 14.52 seconds (JVM running for 15.502)
** Exemple d'exécution d'API Rest **
> curl -X GET http://localhost:8080/memo/id/1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 72 0 72 0 0 4800 0 --:--:-- --:--:-- --:--:-- 4800
{
"title": "memo shopping",
"description": "memo1 description",
"done": false
}
Le répertoire physique et la structure des fichiers du projet sont les suivants. Le répertoire du projet est une structure simple qui stocke pom.xml pour le projet et le répertoire de chaque module. Si vous gérez votre projet avec git, le répertoire du référentiel .git sera également ici. Le répertoire de chaque module a la structure d'un projet maven normal.
/mmsbs
|
+--- /.git
+--- pom.xml
+--- README.md
|
+--- /application <---(1)
| |
| +--- pom.xml
| |
| +--- /src
| | |
| | +--- /main
| | | |
| | | +--- /java
| | | | |
| | | | +--- /com.example
| | | | |
| | | | +--- Application.java <---(2)Point d'entrée de l'application
| | | | +--- WebMvcConfigure.java
| | | | |
| | | | +--- /application
| | | | |
| | | | +--- /config <---(4)
| | | | +--- /controller <---(5)
| | | | +--- /interceptor
| | | | +--- /vo <---(6)
| | | |
| | | +--- /resources
| | | |
| | | +--- application.yml <---(3)Fichier de configuration de l'application
| | | +--- logback-spring.xml
| | | +--- messages.properties
| | |
| | +--- /test
| | |
| | +--- /java
| | | |
| | | +--- /com.example
| | | |
| | | +--- /application
| | | |
| | | +--- /controller <---(7,8)
| | | +--- /vo <---(9)
| | |
| | +--- /resources
| |
| +--- /target
| |
| +--- mmsbs.jar <---(10) executable jar
|
+--- /domain <---(11)
| |
| +--- pom.xml
| |
| +--- /src
| | |
| | +--- /main
| | | |
| | | +--- /java
| | | |
| | | +--- /com.example.domain
| | | |
| | | +--- /config <---(12)
| | | +--- /datasource <---(13)
| | | +--- /entity <---(14)
| | | +--- /repository <---(15)
| | | +--- /service <---(16)
| | | |
| | | +--- /impl <---(16)
| | +--- /test
| | |
| | +--- /java
| | | |
| | | +--- /com.example.domain
| | | |
| | | +--- TestApplication.java <---(17) for testing
| | | |
| | | +--- /repository <---(19,20)
| | | +--- /service <---(21,22)
| | |
| | +--- /resources
| | |
| | +--- application.yml <---(18) for testing
| |
| +--- /target
| |
| +--- domain-0.0.1-SNAPSHOT.jar
|
+--- /common <---(23)
|
+--- pom.xml
|
+--- /src
| |
| +--- /main
| | |
| | +--- /java
| | |
| | +--- /com.example.common
| | |
| | +--- /confing <---(24)
| | +--- /util <---(25)
| +--- /test
| |
| +--- /java
| | |
| | +--- /com.example.common
| | |
| | +--- /util <---(26)
| |
| +--- /resources
|
+--- /target
|
+--- common-0.0.1-SNAPSHOT.jar
Il n'y a pas d'implémentation spéciale dans la configuration multi-modules, c'est une classe de point d'entrée pour les applications Spring Boot ordinaires.
Application
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
En plus des paramètres Spring Boot, les paramètres spécifiques à chaque module sont également décrits. Compte tenu de l'indépendance du module, il est préférable de placer les valeurs de paramétrage spécifiques au module dans le module, mais en privilégiant la commodité, elles sont résumées dans application.yml. Puisqu'il s'agit d'un échantillon, la valeur de réglage n'a pas de signification particulière.
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/sample_db
username: test_user
password: test_user
driverClassName: com.mysql.jdbc.Driver
tomcat:
maxActive: 4
maxIdle: 4
minIdle: 0
initialSize: 4
jpa:
properties:
hibernate:
# show_sql: true
# format_sql: true
# use_sql_comments: true
# generate_statistics: true
jackson:
serialization:
write-dates-as-timestamps: false
server:
port: 8080
logging:
level:
root: INFO
org.springframework: INFO
# application settings
custom:
application:
key1: app_a
key2: app_b
key3: ajToeoe04jtmtU
domain:
key1: domain_c
key2: domain_d
common:
key1: common_e
key2: common_f
datePattern: yyyy-MM-dd
On suppose que la classe qui contient les informations de configuration référencées par la classe implémentée dans le module d'application. Lisez les paramètres du fichier application.yml.
AppConfigure
package com.example.application.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@ConfigurationProperties(prefix = "custom.application")
@Data
@Slf4j
public class AppConfigure {
private String key1;
private String key2;
//private String key3;
@PostConstruct
public void init() {
log.info("AppConfigure init : {}", this);
}
}
C'est une API simple qui répond en traitant les données de la table Memo (transformées en un objet de vue). Le contrôleur dépend du MemoService implémenté dans le module de domaine et de la classe AppConfigure dans le module d'application. À propos, la méthode de gestionnaire appelée id2 est une version qui modifie la valeur de retour de la méthode de gestionnaire appelée id (sans utiliser ResponseEntity).
MemoController
package com.example.application.controller;
import com.example.application.config.AppConfigure;
import com.example.application.vo.MemoView;
import com.example.domain.entity.Memo;
import com.example.domain.service.MemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping(path = "memo", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Slf4j
public class MemoController {
@Autowired
private MemoService service;
@Autowired
private AppConfigure config;
@Value("${custom.application.key3}")
private String key3;
@PostConstruct
public void init() {
log.info("MemoController init : config.key1:{}, config.key2:{}, key3:{}", config.getKey1(), config.getKey2(), key3);
}
@GetMapping(path = "id/{id}")
public ResponseEntity<MemoView> id(@PathVariable(value = "id") Long id) {
log.info("id - id:{}, config.key1:{}, config.key2:{}, key3:{}", id, config.getKey1(), config.getKey2(), key3);
Memo memo = service.findById(id);
return new ResponseEntity<>(convert(memo), HttpStatus.OK);
}
//Modèle qui n'utilise pas ResponseEntity
@GetMapping(path = "id2/{id}")
@ResponseBody
public MemoView id2(@PathVariable(value = "id") Long id) {
log.info("id2 - id:{}, config.key1:{}, config.key2:{}, key3:{}", id, config.getKey1(), config.getKey2(), key3);
Memo memo = service.findById(id);
return convert(memo);
}
@GetMapping(path = "title/{title}")
public ResponseEntity<List<MemoView>> title(@PathVariable(value = "title") String title, Pageable page) {
Page<Memo> memos = service.findByTitle(title, page);
return new ResponseEntity<>(convert(memos.getContent()), HttpStatus.OK);
}
private MemoView convert(final Memo memo) {
return MemoView.from(memo);
}
private List<MemoView> convert(final List<Memo> memos) {
return memos.stream()
.map(MemoView::from)
.collect(Collectors.toList());
}
}
Cette classe est supposée être un objet de vue contenant des informations qui répondent au client. Dans cette application, l'entité n'est pas retournée telle quelle, mais est convertie une fois en objet de vue.
MemoView
package com.example.application.vo;
import com.example.domain.entity.Memo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Value;
import java.io.Serializable;
@Value
@Builder
public class MemoView implements Serializable {
private static final long serialVersionUID = -6945394718471482993L;
private String title;
private String description;
@JsonProperty("completed")
private Boolean done;
public static MemoView from(final Memo memo) {
return MemoView.builder()
.title(memo.getTitle())
.description(memo.getDescription())
.done(memo.getDone())
.build();
}
}
Ceci est un exemple qui sera implémenté comme ceci lors de l'exécution d'un test unitaire de ce contrôleur. Les classes dont dépend le contrôleur sont simulées avec l'annotation MockBean. De plus, il ne se connecte pas à la base de données lorsque le test est exécuté.
MemoControllerTests
package com.example.application.controller;
import com.example.application.config.AppConfigure;
import com.example.application.vo.MemoView;
import com.example.domain.entity.Memo;
import com.example.domain.service.MemoService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@RunWith(SpringRunner.class)
@WebMvcTest(value = MemoController.class, secure = false)
public class MemoControllerTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private MemoService service;
@MockBean
private AppConfigure config;
private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
@Before
public void setup() {
Mockito.when(config.getKey1()).thenReturn("TEST_APP_VALUEA");
Mockito.when(config.getKey2()).thenReturn("TEST_APP_VALUEB");
}
@Test
public void test_id() throws Exception {
Long id = 1L;
LocalDateTime updated = LocalDateTime.of(2017, 9, 20, 13, 14, 15);
Memo expected = Memo.builder().id(id).title("memo").description("memo description").done(false).updated(updated).build();
Mockito.when(service.findById(Mockito.anyLong())).thenReturn(expected);
RequestBuilder builder = MockMvcRequestBuilders
.get("/memo/id/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = mockMvc.perform(builder)
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$").isNotEmpty())
.andExpect(jsonPath("$.title").value(expected.getTitle()))
.andExpect(jsonPath("$.description").value(expected.getDescription()))
.andExpect(jsonPath("$.completed").value(expected.getDone()))
.andDo(print())
.andReturn();
}
@Test
public void test_id2() throws Exception {
Long id = 1L;
LocalDateTime updated = LocalDateTime.of(2017, 9, 20, 13, 14, 15);
Memo expected = Memo.builder().id(id).title("memo").description("memo description").done(false).updated(updated).build();
Mockito.when(service.findById(Mockito.anyLong())).thenReturn(expected);
RequestBuilder builder = MockMvcRequestBuilders
.get("/memo/id2/{id}", id)
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = mockMvc.perform(builder)
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andDo(print())
.andReturn();
MemoView actual = objectMapper.readValue(result.getResponse().getContentAsString(), MemoView.class);
assertThat(actual)
.extracting("title", "description", "done")
.contains(expected.getTitle(), expected.getDescription(), expected.getDone());
}
@Test
public void test_title() throws Exception {
Memo m1 = Memo.builder().id(1L).title("memo1 job").description("memo1 description").done(false).updated(LocalDateTime.now()).build();
Memo m2 = Memo.builder().id(2L).title("memo2 job").description("memo2 description").done(false).updated(LocalDateTime.now()).build();
Memo m3 = Memo.builder().id(3L).title("memo3 job").description("memo3 description").done(false).updated(LocalDateTime.now()).build();
List<Memo> memos = Arrays.asList(m1, m2, m3);
Page<Memo> expected = new PageImpl<>(memos);
Mockito.when(service.findByTitle(Mockito.anyString(), Mockito.any(Pageable.class))).thenReturn(expected);
RequestBuilder builder = MockMvcRequestBuilders
.get("/memo/title/{title}", "job")
.param("page","1")
.param("size", "3")
.accept(MediaType.APPLICATION_JSON_UTF8);
MvcResult result = mockMvc.perform(builder)
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$", hasSize(3)))
.andDo(log())
.andDo(print())
.andReturn();
}
}
Étant donné que MemoController dépend de la classe AppConfigure, vous devez simuler ou injecter l'instance lors de l'exécution d'un test unitaire. La méthode ci-dessus utilise MockBean, mais il existe d'autres façons d'utiliser Import et TestPropertySource.
MemoControllerTests
@RunWith(SpringRunner.class)
@WebMvcTest(value = MemoController.class, secure = false)
@Import(AppConfigure.class)
@TestPropertySource(properties = {
"custom.application.key1=TEST_APP_VALUEA",
"custom.application.key2=TEST_APP_VALUEB"
})
public class MemoControllerTests {
//...réduction
}
Il s'agit d'un exemple d'implémentation lors de l'exécution d'un test d'intégration de ce contrôleur. Il ne se moque pas des classes dont dépend le contrôleur, il se connecte donc à la base de données lors de l'exécution du test. Ce code de test suppose que les données de test sont stockées à l'avance dans la table de la base de données connectée. Utilisez TestRestTemplate pour appeler l'API Rest.
MemoControllerJoinTests
package com.example.application.controller;
import com.example.application.vo.MemoView;
import org.assertj.core.groups.Tuple;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MemoControllerJoinTests {
@Autowired
private TestRestTemplate restTemplate;
private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
@Test
public void test_id() {
MemoView expected = MemoView.builder().title("memo shopping").description("memo1 description").done(false).build();
Map<String, Object> params = new HashMap<>();
params.put("id", 1L);
ResponseEntity<MemoView> actual = restTemplate.getForEntity("/memo/id/{id}", MemoView.class, params);
assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(actual.getHeaders().getContentType()).isEqualTo(contentType);
assertThat(actual.getBody()).isEqualTo(expected);
}
@Test
public void test_id2() {
MemoView expected = MemoView.builder().title("memo shopping").description("memo1 description").done(false).build();
Map<String, Object> params = new HashMap<>();
params.put("id", 1L);
MemoView actual = restTemplate.getForObject("/memo/id2/{id}", MemoView.class, params);
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_title() {
RequestEntity requestEntity = RequestEntity.get(URI.create("/memo/title/job?page=1&size=3&sort=id,desc")).build();
ResponseEntity<List<MemoView>> actual = restTemplate.exchange(requestEntity,
new ParameterizedTypeReference<List<MemoView>>(){});
assertThat(actual.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(actual.getHeaders().getContentType()).isEqualTo(contentType);
assertThat(actual.getBody())
.extracting("title", "description", "done")
.containsExactly(
Tuple.tuple("memo job", "memo4 description", false),
Tuple.tuple("memo job", "memo2 description", false)
);
}
}
Il s'agit d'un exemple d'implémentation pour le test unitaire des objets de vue. Vérifiez que l'objet de vue peut être converti en objet JSON attendu.
MemoViewTests
package com.example.application.vo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;
import org.springframework.boot.test.json.ObjectContent;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
@RunWith(SpringRunner.class)
@JsonTest
public class MemoViewTests {
@Autowired
private JacksonTester<MemoView> json;
@Test
public void test_serialize() throws IOException {
String expected = "{\"title\":\"memo\",\"description\":\"memo description\",\"completed\":false}";
MemoView memoView = MemoView.builder().title("memo").description("memo description").done(false).build();
JsonContent<MemoView> actual = json.write(memoView);
actual.assertThat().isEqualTo(expected);
actual.assertThat().hasJsonPathStringValue("$.title");
actual.assertThat().extractingJsonPathStringValue("$.title").isEqualTo("memo");
actual.assertThat().hasJsonPathStringValue("$.description");
actual.assertThat().extractingJsonPathStringValue("$.description").isEqualTo("memo description");
actual.assertThat().hasJsonPathBooleanValue("$.completed");
actual.assertThat().extractingJsonPathBooleanValue("$.completed").isEqualTo(false);
}
@Test
public void test_deserialize() throws IOException {
MemoView expected = MemoView.builder().title("memo").description("memo description").done(false).build();
String content = "{\"title\":\"memo\",\"description\":\"memo description\",\"completed\":false}";
ObjectContent<MemoView> actual = json.parse(content);
actual.assertThat().isEqualTo(expected);
}
}
Si la construction réussit, un fichier appelé mmsbs.jar
sera généré sous le répertoire cible.
On suppose que la classe qui contient les informations de configuration référencées par la classe implémentée dans le module de domaine. Lisez les paramètres du fichier application.yml.
DomainConfigure
package com.example.domain.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@ConfigurationProperties(prefix = "custom.domain")
@Data
@Slf4j
public class DomainConfigure {
private String key1;
private String key2;
@PostConstruct
public void init() {
log.info("DomainConfigure init : {}", this);
}
}
Étant donné que le module de domaine est en charge du traitement de l'accès aux données, il existe une classe qui définit la source de données. Les paramètres de la source de données font référence à application.yml.
DataSourceConfigure
package com.example.domain.datasource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = {"com.example.domain.repository"}
)
public class DataSourceConfigure {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource datasource() {
DataSource dataSource = DataSourceBuilder.create().build();
return dataSource;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder) {
LocalContainerEntityManagerFactoryBean factory = builder
.dataSource(datasource())
.persistenceUnit("default")
.packages("com.example.domain.entity")
.build();
return factory;
}
@Bean
public PlatformTransactionManager transactionManager(
EntityManagerFactory entityManagerFactory) {
JpaTransactionManager tm = new JpaTransactionManager();
tm.setEntityManagerFactory(entityManagerFactory);
tm.afterPropertiesSet();
return tm;
}
}
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;
données de test
INSERT INTO memo (id, title, description, done, updated) VALUES
(1, 'memo shopping', 'memo1 description', false, '2017-09-20 12:01:00.123'),
(2, 'memo job', 'memo2 description', false, '2017-09-20 13:02:10.345'),
(3, 'memo private', 'memo3 description', false, '2017-09-20 14:03:21.567'),
(4, 'memo job', 'memo4 description', false, '2017-09-20 15:04:32.789'),
(5, 'memo private', 'memo5 description', false, '2017-09-20 16:05:43.901'),
(6, 'memo travel', 'memo6 description', false, '2017-09-20 17:06:54.234'),
(7, 'memo travel', 'memo7 description', false, '2017-09-20 18:07:05.456'),
(8, 'memo shopping', 'memo8 description', false, '2017-09-20 19:08:16.678'),
(9, 'memo private', 'memo9 description', false, '2017-09-20 20:09:27.890'),
(10,'memo hospital', 'memoA description', false, '2017-09-20 21:10:38.012')
;
MemoRepository
package com.example.domain.repository;
import com.example.domain.entity.Memo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemoRepository extends JpaRepository<Memo, Long> {
Page<Memo> findByTitleLike(String title, Pageable page);
}
interface
MemoService
package com.example.domain.service;
import com.example.domain.entity.Memo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.time.LocalDate;
public interface MemoService {
Memo findById(Long id);
Page<Memo> findByTitle(String title, Pageable page);
Memo registerWeatherMemo(LocalDate date);
}
** Classe d'implémentation **
Ce service dépend d'une classe appelée WeatherForecast implémentée dans le module commun. Puisqu'il s'agit d'un échantillon, il n'y a pas de signification particulière dans le contenu d'implémentation du service.
MemoServiceImpl
package com.example.domain.service.impl;
import com.example.common.util.WeatherForecast;
import com.example.domain.config.DomainConfigure;
import com.example.domain.entity.Memo;
import com.example.domain.repository.MemoRepository;
import com.example.domain.service.MemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Service
@Slf4j
public class MemoServiceImpl implements MemoService {
@Autowired
private DomainConfigure config;
@Autowired
private MemoRepository memoRepository;
@Autowired
private WeatherForecast weatherForecast;
@Transactional(readOnly = true)
@Override
public Memo findById(Long id) {
log.info("findById - id:{}, config.key1:{}, config.key2:{}", id, config.getKey1(), config.getKey2());
return memoRepository.findOne(id);
}
@Transactional(readOnly = true)
@Override
public Page<Memo> findByTitle(String title, Pageable page) {
log.info("findByTitle - title:{}, page:{}, config.key1:{}, config.key2:{}", title, page, config.getKey1(), config.getKey2());
return memoRepository.findByTitleLike(String.join("","%", title, "%"), page);
}
@Transactional(timeout = 10)
@Override
public Memo registerWeatherMemo(LocalDate date) {
log.info("registerWeatherMemo - date:{}", date);
String title = "weather memo : [" + weatherForecast.getReportDayStringValue(date) + "]";
String description = weatherForecast.report(date);
Memo memo = Memo.builder().title(title).description(description).build();
return memoRepository.saveAndFlush(memo);
}
}
Il s'agit d'une classe de point d'entrée pour l'environnement de test qui est requise lors des tests avec le module de domaine. Étant donné que certaines classes d'implémentation dépendent du module commun, ajoutez le package de module commun à scanBasePackages.
TestApplication
package com.example.domain;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {
"com.example.domain",
"com.example.common"
})
public class TestApplication {
public static void main(String... args) {
SpringApplication.run(TestApplication.class, args);
}
}
Application.yml avec les informations de configuration qui seront valides dans l'environnement de test. Étant donné que certains cas de test nécessitent une connexion à la base de données, nous définissons également la source de données.
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/sample_db
username: test_user
password: test_user
driverClassName: com.mysql.jdbc.Driver
jpa:
properties:
hibernate:
show_sql: true
custom:
domain:
key1: test_domain_c
key2: test_domain_d
Il s'agit d'un exemple d'implémentation pour les tests unitaires d'un référentiel. L'ajout de l'annotation DataJpaTest à la classe de test utilisera H2 en mémoire au moment de l'exécution.
MemoRepositoryTests
package com.example.domain.repository;
import com.example.domain.entity.Memo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@DataJpaTest
public class MemoRepositoryTests {
@Autowired
private TestEntityManager entityManager;
@Autowired
private MemoRepository sut;
@BeforeTransaction
public void init() {
//Exécuté avant le début de la transaction
System.out.println("1. init");
}
@Before
public void setUp() {
//Exécuté après le début de la transaction et avant le début de la méthode de test
System.out.println("2. setUp");
}
@After
public void tearDown() {
//Exécuté après la fin de la méthode de test et avant la fin de la transaction
System.out.println("3. tearDown");
}
@AfterTransaction
public void clear() {
//Exécuté après la fin de la transaction
System.out.println("4. clear");
}
@Test
@Sql(statements = {
"INSERT INTO memo (id, title, description, done, updated) VALUES (99999, 'memo test', 'memo description', FALSE, CURRENT_TIMESTAMP)"
})
public void test_findOne() {
Memo expected = entityManager.find(Memo.class, 99999L);
Memo actual = sut.findOne(expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_save() {
Memo expected = Memo.builder().title("memo").description("memo description").build();
sut.saveAndFlush(expected);
entityManager.clear();
Memo actual = entityManager.find(Memo.class, expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_findByTitleLike() {
Memo m1 = Memo.builder().title("memo shopping").description("memo1 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m1);
Memo m2 = Memo.builder().title("memo job").description("memo2 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m2);
Memo m3 = Memo.builder().title("memo private").description("memo3 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m3);
Memo m4 = Memo.builder().title("memo job").description("memo4 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m4);
Memo m5 = Memo.builder().title("memo private").description("memo5 description").done(false).updated(LocalDateTime.now()).build();
entityManager.persistAndFlush(m5);
entityManager.clear();
List<Memo> expected = Arrays.asList(m4, m2);
Pageable page = new PageRequest(0, 3, Sort.Direction.DESC, "id");
Page<Memo> actual = sut.findByTitleLike("%job%", page);
assertThat(actual.getContent()).isEqualTo(expected);
}
}
Il s'agit d'un exemple d'implémentation pour les tests d'intégration de référentiel. Je ne pense pas qu'il y ait beaucoup de tests d'intégration de référentiel, mais si vous voulez vous connecter à une base de données externe (MySQL ou PostgreSQL) pour une raison quelconque, c'est l'implémentation.
MemoRepositoryJoinTests
package com.example.domain.repository;
import com.example.domain.entity.Memo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemoRepositoryJoinTests {
@Autowired
private EntityManager entityManager;
@Autowired
private MemoRepository sut;
@Transactional
@Test
@Sql(statements = {
"INSERT INTO memo (id, title, description, done) VALUES (99999, 'memo test', 'memo description', TRUE)"
})
public void test_findOne() {
Memo expected = entityManager.find(Memo.class, 99999L);
Memo actual = sut.findOne(expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Transactional
@Test
public void test_save() {
Memo expected = Memo.builder().title("memo").description("memo description").build();
sut.saveAndFlush(expected);
entityManager.clear();
Memo actual = entityManager.find(Memo.class, expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Transactional
@Test
public void test_findByTitleLike() {
Memo m1 = entityManager.find(Memo.class, 2L);
Memo m2 = entityManager.find(Memo.class, 4L);
List<Memo> expected = Arrays.asList(m2, m1);
Pageable page = new PageRequest(0, 3, Sort.Direction.DESC, "id");
Page<Memo> actual = sut.findByTitleLike("%job%", page);
assertThat(actual.getContent()).isEqualTo(expected);
}
}
Il peut être associé à une classe de test ou à une méthode de test. Vous pouvez préparer les données de test en exécutant un fichier de script SQL ou une instruction SQL.
Notez que si vous ajoutez l'annotation SQL à la fois à la classe et à la méthode, les paramètres au niveau de la méthode prendront effet. (Il ne peut pas être utilisé pour saisir des données communes au niveau de la classe et des données spécifiques à la méthode (différence) au niveau de la méthode.)
Voici un exemple d'une telle implémentation lors de l'exécution d'un test unitaire d'un service. Les classes dépendantes sont ridiculisées. Cet exemple n'utilise pas la fonction de conteneur de Spring, mais utilise JUnit, Mockito et AssertJ.
MemoServiceTests
package com.example.domain.service;
import com.example.common.config.CommonConfigure;
import com.example.common.util.WeatherForecast;
import com.example.domain.config.DomainConfigure;
import com.example.domain.entity.Memo;
import com.example.domain.repository.MemoRepository;
import com.example.domain.service.impl.MemoServiceImpl;
import org.junit.Before;
import org.junit.Test;
import org.mockito.*;
import org.mockito.internal.util.reflection.Whitebox;
import org.springframework.data.domain.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
public class MemoServiceTests {
@Mock
private MemoRepository repository;
@Spy
private WeatherForecast weatherForecast;
@InjectMocks
private MemoServiceImpl sut;
@Before
public void setup(){
MockitoAnnotations.initMocks(this);
DomainConfigure config = new DomainConfigure();
config.setKey1("bean_domain_c");
config.setKey2("bean_domain_d");
Whitebox.setInternalState(sut, "config", config);
CommonConfigure commonConfigure = new CommonConfigure();
commonConfigure.setDatePattern("yyyy-MM-dd");
Whitebox.setInternalState(weatherForecast, "config", commonConfigure);
}
@Test
public void test_findById() {
Memo expected = Memo.builder().id(1L).title("memo").description("memo description").done(false).updated(LocalDateTime.now()).build();
Mockito.when(repository.findOne(Mockito.anyLong())).thenReturn(expected);
Memo actual = sut.findById(expected.getId());
assertThat(actual).isEqualTo(expected);
}
@Test
public void test_findByTitle() {
Memo m1 = Memo.builder().id(2L).title("memo job").description("memo2 description").done(false).updated(LocalDateTime.now()).build();
Memo m2 = Memo.builder().id(4L).title("memo job").description("memo4 description").done(false).updated(LocalDateTime.now()).build();
List<Memo> memos = Arrays.asList(m2, m1);
Page<Memo> expected = new PageImpl<>(memos);
String title = "job";
Pageable page = new PageRequest(0,3, Sort.Direction.DESC, "id");
Mockito.when(repository.findByTitleLike(eq("%" + title + "%"), eq(page))).thenReturn(expected);
Page<Memo> actual = sut.findByTitle(title, page);
assertThat(actual.getContent()).isEqualTo(expected.getContent());
}
@Test
public void test_registerWeatherMemo() {
LocalDate date = LocalDate.of(2017, 9, 20);
Mockito.when(weatherForecast.report(date)).thenReturn("weather forecast : test-test-test-2017-09-20");
Memo expected = Memo.builder().id(1L).title("weather memo :").description("weather forecast : sunny").done(false).updated(LocalDateTime.now()).build();
Mockito.when(repository.saveAndFlush(any(Memo.class))).thenReturn(expected);
Memo actual = sut.registerWeatherMemo(date);
assertThat(actual).isEqualTo(expected);
}
}
En plus d'utiliser Whitebox.setInternalState ci-dessus, vous pouvez simuler DomainConfigure à l'aide de ReflectionTestUtils.
@Before
public void setup(){
MockitoAnnotations.initMocks(this);
DomainConfigure config = new DomainConfigure();
config.setKey1("bean_domain_c");
config.setKey2("bean_domain_d");
ReflectionTestUtils.setField(sut, "config", config);
}
Il s'agit d'un exemple d'implémentation pour les tests d'intégration de services. Les classes dont dépend le service à tester ne sont pas simulées (certaines sont SpyBean), elles se connectent donc également à la base de données. En outre, les valeurs de propriété référencées par la classe WeatherForecast du module commun dont dépend ce service doivent être définies à l'aide de TestPropertySource.
MemoServiceJoinTests
package com.example.domain.service;
import com.example.common.util.WeatherForecast;
import com.example.domain.TestApplication;
import com.example.domain.datasource.DataSourceConfigure;
import com.example.domain.entity.Memo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = {
TestApplication.class, DataSourceConfigure.class})
@TestPropertySource(properties = {
"custom.common.datePattern=yyyy-MM-dd"
})
public class MemoServiceJoinTests {
@Autowired
private EntityManager entityManager;
@Autowired
private MemoService sut;
@SpyBean
private WeatherForecast weatherForecast;
@Transactional
@Test
public void test_findById() {
Long id = 1L;
Memo expected = entityManager.find(Memo.class, id);
Memo actual = sut.findById(id);
assertThat(actual).isEqualTo(expected);
}
@Transactional
@Test
public void test_findByTitle() {
Memo m1 = entityManager.find(Memo.class, 2L);
Memo m2 = entityManager.find(Memo.class, 4L);
List<Memo> expected = Arrays.asList(m2, m1);
Pageable page = new PageRequest(0,3, Sort.Direction.DESC, "id");
Page<Memo> actual = sut.findByTitle("job", page);
assertThat(actual.getContent()).isEqualTo(expected);
}
@Transactional
@Test
public void test_registerWeatherMemo() {
LocalDate date = LocalDate.of(2017,9,20);
Mockito.when(weatherForecast.report(date)).thenReturn("weather forecast : test-test-test");
Memo actual = sut.registerWeatherMemo(date);
assertThat(actual.getId()).isNotNull();
assertThat(actual.getTitle()).isEqualTo("weather memo : [2017-09-20]");
assertThat(actual.getDescription()).isEqualTo("weather forecast : test-test-test");
}
}
On suppose que la classe qui contient les informations de configuration référencées par la classe implémentée dans le module commun. Lisez les paramètres du fichier application.yml.
CommonConfigure
package com.example.common.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
@ConfigurationProperties(prefix = "custom.common")
@Data
@Slf4j
public class CommonConfigure {
private String key1;
private String key2;
private String datePattern;
@PostConstruct
public void init() {
log.info("CommonConfigure init : {}", this);
}
}
Cette classe utilitaire n'a pas de signification particulière car il s'agit d'une implémentation pour créer des dépendances entre les modules. Pour le moment, on suppose qu'il s'agit d'une classe utilitaire qui appelle un service Web externe pour effectuer des prévisions météorologiques.
WeatherForecast
package com.example.common.util;
import com.example.common.config.CommonConfigure;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component
@Slf4j
public class WeatherForecast {
@Autowired
private CommonConfigure config;
public String getReportDayStringValue(LocalDate reportDay) {
log.debug("getReportDayStringValue - reportDay:{}, config:{}", reportDay, config);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(config.getDatePattern());
return reportDay.format(formatter);
}
public String report(LocalDate reportDay) {
log.debug("report - reportDay:{}", reportDay);
String weather = "weather forecast : " + callForecastApi(reportDay);
return weather;
}
String callForecastApi(LocalDate date) {
// call External Weather API
String apiResult = UUID.randomUUID().toString();
String dateStr = date.toString();
return apiResult + "-" + dateStr;
}
}
Ceci est un exemple qui sera implémenté comme ceci lors de l'exécution d'un test unitaire de cette classe d'utilitaire.
WeatherForecastTests
package com.example.common.util;
import com.example.common.config.CommonConfigure;
import org.junit.Before;
import org.junit.Test;
import org.mockito.*;
import org.mockito.internal.util.reflection.Whitebox;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThat;
public class WeatherForecastTests {
@Spy
@InjectMocks
private WeatherForecast sut;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
CommonConfigure config = new CommonConfigure();
config.setKey1("test_common_e");
config.setKey2("test_common_f");
config.setDatePattern("yyyy/MM/dd");
Whitebox.setInternalState(sut, "config", config);
}
@Test
public void test_getReportDayStringValue() {
LocalDate date = LocalDate.of(2017, 9, 20);
String actual = sut.getReportDayStringValue(date);
assertThat(actual).isEqualTo("2017/09/20");
}
@Test
public void test_report() {
LocalDate date = LocalDate.of(2017, 9, 20);
Mockito.when(sut.callForecastApi(date)).thenReturn("test-test-test");
String actual = sut.report(date);
assertThat(actual).isEqualTo("weather forecast : test-test-test");
}
}
Dans l'exemple de cet article, le projet est spécifié pour le parent de chaque module, mais si vous regardez le projet de structure multi module sur github, etc., chaque module spécifie également spring-boot-starter-parent pour le parent. fait.
Lors de la comparaison des instances d'une entité qui contient un champ de type Date comme indiqué ci-dessous, l'assertion peut échouer car la représentation sous forme de chaîne de la date est différente.
En effet, la classe d'implémentation de l'instance du champ de type Date de l'entité renvoyée par Hibernate est de type java.sql.Timestamp
.
@Column(name="updated", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private java.util.Date updated;
org.junit.ComparisonFailure:
Expected :Memo{id=2, title='title', description='desc', done=false, updated=Wed Sep 20 10:34:21 JST 2017}
Actual :Memo{id=2, title='title', description='desc', done=false, updated=2017-09-20 10:34:21.853}
JUnit
annotation | package |
---|---|
RunWith | org.junit.runner.RunWith |
Test | org.junit.Test |
Before | org.junit.Before |
After | org.junit.After |
Spring
annotation | package |
---|---|
SpringBootTest | org.springframework.boot.test.context.SpringBootTest |
ContextConfiguration | org.springframework.test.context.ContextConfiguration |
TestPropertySource | org.springframework.test.context.TestPropertySource |
WebMvcTest | org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest |
DataJpaTest | org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest |
JsonTest | org.springframework.boot.test.autoconfigure.json.JsonTest |
MockBean | org.springframework.boot.test.mock.mockito.MockBean |
SpyBean | org.springframework.boot.test.mock.mockito.SpyBean |
BeforeTransaction | org.springframework.test.context.transaction.BeforeTransaction |
AfterTransaction | org.springframework.test.context.transaction.AfterTransaction |
Sql | org.springframework.test.context.jdbc.Sql |
MockBean et SpyBean sont utilisés pour les objets moqués et espionnés Autowired.
Mockito
annotation | package |
---|---|
InjectMocks | org.mockito.InjectMocks |
Mock | org.mockito.Mock |
Spy | org.mockito.Spy |
Recommended Posts