Dans cet article, je vais vous expliquer comment effectuer un test unitaire à l'aide de Mockito et JUnit. L'exemple est créé par Spring Boot et utilise l'injection de constructeur. Par conséquent, les annotations telles que «@ Mock» et «@ InjectMock» n'apparaissent pas.
Mockito est un framework de test open source pour Java publié sous la licence MIT. (par Wikipedia) ~~ Lire comme Mock Ito. ~~ Lire comme Moquito. Vous pouvez facilement créer un objet fictif pour le test en l'utilisant.
Il est difficile d'écrire un test pour les tests suivants.
--Branch l'opération en fonction de la valeur de DB → Réécrivez-vous le DB dans le test? Comment préparez-vous le DB pour le test? Est-il initialisé avec la même valeur à chaque fois?
Vous pouvez résoudre ces problèmes en remplaçant la classe que vous souhaitez tester par une classe (simulée) qui se comporte de la même manière que la classe dépendante. Les avantages sont les suivants.
plugins {
id 'org.springframework.boot' version '2.3.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.mockito'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
test {
useJUnitPlatform()
}
Mockito est inclus dans le test de stérilisation Spring Boot, il n'est donc pas nécessaire d'ajouter quoi que ce soit des valeurs par défaut de Spring Boot. Je l'ai ajouté parce que je voulais utiliser uniquement Lombok.
Prenons l'exemple du système de point de vente qui calcule le prix total des produits. Le montant total est calculé en ajoutant le taux de taxe défini pour chaque type de produit. Le taux d'imposition est susceptible de changer à l'avenir, il doit donc être obtenu auprès de la DB.
Définition d'objet produit
package com.example.mockito;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
*Classification des produits
*
* @author rcftdbeu
*
*/
enum ItemType {
Food, Other
}
/**
*Produit
*
* @author rcftdbeu
*
*/
@Getter
@Setter
@AllArgsConstructor
public class Item {
/**Nom*/
String name;
/**prix*/
int price;
/**type*/
ItemType itemType;
}
Service pour calculer le montant total
package com.example.mockito;
import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class POS {
private final TaxRateMaster taxRateMaster;
/**
*Calculez la quantité totale de marchandises
*
* @param itemList
* @return
*/
public int culculateTotal(List<Item> itemList) {
//montant total
int sum = 0;
//Le taux de taxe est calculé et ajouté un par un
for (Item item : itemList) {
sum += item.getPrice() * (1 + taxRateMaster.getTaxRate(item.getItemType()));
}
return sum;
}
}
Classe qui contient l'accès à la base de données à simuler
package com.example.mockito;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
/**
*Obtenez le taux d'imposition
*
* @author rcftdbeu
*
*/
@RequiredArgsConstructor
@Component
public class TaxRateMaster {
private final TaxRepository ripository;
/**
*La date d'aujourd'hui
*
* @return
*/
public Date getSysDate() {
return new Date();
}
/**
*Est-ce après la date d'application du taux d'imposition réduit?
*
* @return
*/
private boolean isAfterApplyDate() {
String strDate = "2019/10/1 00:00:00";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
try {
Date applyDate = dateFormat.parse(strDate);
return getSysDate().after(applyDate);
} catch (Exception e) {
}
return false;
}
/**
*Obtenez le taux d'imposition
*
* @param type
* @return
*/
public Double getTaxRate(ItemType type) {
//Après application du taux d'imposition réduit
if (isAfterApplyDate()) {
if (type == ItemType.Food) {
return ripository.getFoodTaxRate();
} else {
return ripository.getOtherTaxRate();
}
}
//Avant d'appliquer le taux d'imposition réduit
return ripository.getOldTaxRate();
}
}
Dépôt
package com.example.mockito;
import org.springframework.stereotype.Repository;
/**
*Processus pour obtenir le taux d'imposition de DB<br>
*Non implémenté car DB n'est pas utilisé<br>
*Existe uniquement pour éviter les erreurs de compilation
*
* @author rcftdbeu
*
*/
@Repository
public class TaxRepository {
public Double getFoodTaxRate() {
return null;
}
public Double getOtherTaxRate() {
return null;
}
public Double getOldTaxRate() {
return null;
}
}
Unit Test
Enfin le sujet principal.
Il existe deux types disponibles dans Mockito: «mock» et «spy».
mock: Un simulacre dans lequel toutes les méthodes de la classe cible sont remplacées par return null
. Aucune initialisation requise.
spy: nécessite une initialisation avec le même objet que la classe cible. Seules des méthodes spécifiques peuvent être modifiées selon les besoins.
Mock
Nous réaliserons un test en utilisant des simulations.
package com.example.mockito;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
public class MockTest {
private static POS pos;
private static TaxRateMaster master;
private static TaxRepository repository;
@Nested
Test avec classe statique Mock{
//Exécuter une seule fois avant le test
@BeforeAll
static void init() {
master = mock(TaxRateMaster.class);
pos = new POS(master);
}
//Exécuter avant chaque test
@BeforeEach
void setup() {
//Le taux de la taxe d'épicerie est de 8%
when(master.getTaxRate(ItemType.Food)).thenReturn(0.08);
//Les autres taux d'imposition sont de 10%
when(master.getTaxRate(ItemType.Other)).thenReturn(0.10);
}
@Test
void Taux de taxe réduit calculé pour les produits d'épicerie_Calculé à 8%() {
//Ajouter un gâteau de riz de 100 yens
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("balle de riz", 100, ItemType.Food);
itemList.add(onigiri);
//Calcul
int sum = pos.culculateTotal(itemList);
//Vérifiez le résultat
assertThat(sum, is(108));
}
@Test
nul autre calcul du taux d'imposition réduit_Calculé à 10%() {
//Magazine de 500 yens ajouté
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("magazine", 500, ItemType.Other);
itemList.add(onigiri);
//Calcul
int sum = pos.culculateTotal(itemList);
//Vérifiez le résultat
assertThat(sum, is(550));
}
}
}
init()
La maquette est en cours d'initialisation. Le TaxRateMaster dont dépend le POS est remplacé par un simulacre. La structure est la suivante. Fausse entité POS-TaxRateMaster
setup()
Il définit le comportement du simulacre.
when ()
définit quelle méthode est appelée et thenReturn ()
retourne quoi.
Si vous souhaitez définir le comportement lors de l'exécution avec un argument spécifique, spécifiez directement cette valeur. Pour définir le comportement lorsqu'il est exécuté avec des arguments arbitraires, spécifiez anyString () ou any (ItemType.class).
Il existe deux méthodes, l'une consiste à écrire avec when (). ThenReturn ()
et l'autre à écrire avec doReturn (). When ()
.
Si vous réécrivez l'exemple de configuration, ce sera doReturn (0.08) .when (master) .getTaxRate (ItemType.Food);
.
Ce qui est mieux est controversé.
quand (). thenReturn () faction --⭕️ Facile à lire dans le même ordre que les instructions if -❌ Peut ne pas être disponible avec espion --❌ thenThrow n'est pas disponible pour les méthodes qui retournent void → Si vous concevez correctement, vous n'aurez pas la possibilité d'utiliser doReturn.
doReturn (). Quand () faction --⭕️ Peut être utilisé à tout moment sans souci -❌ La valeur décrite dans doReturn () devient le type Object et le compilateur ne peut pas détecter l'erreur. → Il est plus facile de s'unifier avec doReturn si vous vous souvenez des différences subtiles, et vous pouvez le découvrir en exécutant la vérification de type.
Personnellement, j'aime bien when (). ThenReturn (), qui est facile à lire, donc j'utilise également cette notation dans cet article.
Il n'y a rien de spécial, créez simplement une instance Item pour tester et exécutez culculateTotal pour être testé.
Spy
@Nested
Test avec la classe statique Spy{
//Exécuter une seule fois avant le test
@BeforeAll
static void init() {
repository = mock(TaxRepository.class);
master = spy(new TaxRateMaster(repository));
pos = new POS(master);
}
//Exécuter avant chaque test
@BeforeEach
void setup() {
//Le taux d'imposition avant l'application du taux d'imposition réduit est de 8%
when(repository.getOldTaxRate()).thenReturn(0.08);
//Le taux de la taxe d'épicerie est de 8%
when(repository.getFoodTaxRate()).thenReturn(0.08);
//Les autres taux d'imposition sont de 10%
when(repository.getOtherTaxRate()).thenReturn(0.10);
}
@Nested
classe Avant d'appliquer le taux d'imposition réduit{
@BeforeEach
void setup() throws ParseException {
//Défini pour renvoyer la veille de l'application du taux de taxe réduit lorsque la date actuelle est acquise
String strDate = "2019/9/30";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
Date applyDate = dateFormat.parse(strDate);
when(master.getSysDate()).thenReturn(applyDate);
}
@Test
void Taux de taxe réduit calculé pour les produits d'épicerie_Calculé à 8%() {
//Ajouter un gâteau de riz de 100 yens
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("balle de riz", 100, ItemType.Food);
itemList.add(onigiri);
//Calcul
int sum = pos.culculateTotal(itemList);
//Vérifiez le résultat
assertThat(sum, is(108));
}
@Test
nul autre calcul du taux d'imposition réduit_Calculé à 8%() {
//Magazine de 500 yens ajouté
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("magazine", 500, ItemType.Other);
itemList.add(onigiri);
//Calcul
int sum = pos.culculateTotal(itemList);
//Vérifiez le résultat
assertThat(sum, is(540));
}
}
@Nested
classe Après application du taux d'imposition réduit{
@BeforeEach
void setup() throws ParseException {
//Défini pour renvoyer la veille de l'application du taux de taxe réduit lorsque la date actuelle est acquise
String strDate = "2019/10/1 00:00:01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date applyDate = dateFormat.parse(strDate);
when(master.getSysDate()).thenReturn(applyDate);
}
@Test
void Taux de taxe réduit calculé pour les produits d'épicerie_Calculé à 8%() {
//Ajouter un gâteau de riz de 100 yens
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("balle de riz", 100, ItemType.Food);
itemList.add(onigiri);
//Calcul
int sum = pos.culculateTotal(itemList);
//Vérifiez le résultat
assertThat(sum, is(108));
}
@Test
nul autre calcul du taux d'imposition réduit_Calculé à 10%() {
//Magazine de 500 yens ajouté
List<Item> itemList = new ArrayList<Item>();
Item onigiri = new Item("magazine", 500, ItemType.Other);
itemList.add(onigiri);
//Calcul
int sum = pos.culculateTotal(itemList);
//Vérifiez le résultat
assertThat(sum, is(550));
}
}
}
init()
Initialisation de Spy. Définissez TaxRateMaster dont le point de vente dépend de Spy et réglez-le sur Spy. La structure est la suivante. Simulation de Spy-TaxRepository de POS entity-TaxRateMaster
setup()
Seule la partie qui acquiert la date actuelle dans Spy of TaxRateMaster est remplacée par la date définie.
C'est la même chose que le cas du simulacre, et il n'y a pas d'explication particulière.
when(master.getTaxRate(ItemType.Food)).thenReturn(0.08)
.thenReturn(0.10);
En connectant thenReturn
ensemble, différentes valeurs peuvent être renvoyées dans les premier et deuxième appels.
La première fois peut être utilisée pour l'enregistrement et la deuxième fois pour la mise à jour.
verify(master).getTaxRate(ItemType.Food);
Vous pouvez vérifier que la méthode fictive a été appelée en utilisant verify
.
Vous pouvez également vérifier que l'argument au moment de l'exécution de la méthode est exécuté avec l'argument spécifié.
@BeforeEach
void setup() {
clearInvocations(master);
}
Il n'y a qu'une seule instance fictive, et si elle est exécutée à partir de plusieurs tests, le nombre d'appels dans d'autres tests sera compté lors de la vérification avec verify
, donc avant d'exécuter le test, utilisez clearInvocations
pour compter le nombre d'appels simulés. Vous devez le réinitialiser.
verify(master, times(2)).getTaxRate(ItemType.Food);
Vous pouvez vérifier que la méthode a été appelée le nombre de fois spécifié en ajoutant times
à l'argument de verify
.
verify(master, never()).getTaxRate(ItemType.Food);
Vous pouvez vérifier que la méthode n'est pas appelée en ajoutant never
à l'argument de verify
.
La même chose est vraie pour «times (0)».
Si l'argument de la méthode que vous souhaitez vérifier est un type primitif tel que String ou Boolean, vous pouvez le vérifier avec l'argument de la méthode après vérification, mais pour d'autres objets, Vous pouvez obtenir les arguments en utilisant ArgumentCaptor.
@Test
Obtenir l'argument nul() {
//Générer ArgumentCaptor pour obtenir des arguments
ArgumentCaptor<ItemType> argCaptor = ArgumentCaptor.forClass(ItemType.class);
//Ajouter un gâteau de riz de 100 yens
List<Item> itemList = new ArrayList<Item>();
itemList.add(new Item("balle de riz", 100, ItemType.Food));
//Exécution du calcul
pos.culculateTotal(itemList);
//Obtenir des arguments
verify(master).getTaxRate(argCaptor.capture());
ItemType executedItemType = argCaptor.getValue();
//Valider les arguments d'exécution
assertThat(executedItemType, is(ItemType.Food));
}
@Test
void Une exception s'est produite() {
when(master.getTaxRate(ItemType.Food)).thenThrow(new RuntimeException("Échec de la connexion à la base de données"));
//Ajouter un gâteau de riz de 100 yens
List<Item> itemList = new ArrayList<Item>();
itemList.add(new Item("balle de riz", 100, ItemType.Food));
//Calcul
assertThrows(RuntimeException.class, () -> pos.culculateTotal(itemList));
//puis effacez les paramètres de lancer
reset(master);
}
Vous pouvez lever une exception en définissant thenThrow
au lieu de thenReturn
.
Les exceptions ne peuvent être définies que pour les exceptions levées par la méthode correspondante. Comme aucune exception n'est définie dans cette méthode, RuntimeException
est levée.
De plus, je ne veux pas lancer d'exception dans d'autres tests, je l'ai donc effacée avec reset
.
Par défaut, il ne peut pas être moqué. La méthode statique est incompatible avec la simulation, veuillez donc envisager de la rendre non statique si possible. Il semble qu'il puisse être converti en mock en utilisant Power mock, mais je ne savais pas comment le faire fonctionner avec JUnit5.
Cela ne peut pas non plus être moqué. Les méthodes privées devraient pouvoir être testées via des méthodes publiques, alors implémentons des tests pour les méthodes publiques. Si vous le souhaitez vraiment, vous devrez utiliser Power Mock ou essayer la magie noire comme la réflexion.
Recommended Posts