Test unitaire Java avec Mockito

Test unitaire Java avec Mockito

Objectif

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.

Qu'est-ce que Mockito

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.

Qu'est-ce qui vous rend heureux avec une simulation?

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.

Matériel

Installation

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.

Cible de test

Aperçu

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).

À propos de la notation

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.

Lancer le test

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.

Lancer le test

C'est la même chose que le cas du simulacre, et il n'y a pas d'explication particulière.

Diverses autres utilisations

Modifier la valeur renvoyée la première et la deuxième fois

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.

Vérifiez que la méthode a été appelée

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é.

Effacer le nombre d'appels de méthode

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

Vérifiez que la méthode a été appelée plusieurs fois

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.

Vérifiez que la méthode n'a pas été appelée

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)».

Validez l'argument

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

Vérifiez le comportement lorsqu'une exception se produit

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

Autre test lié

Je veux moquer la méthode statique

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.

Je souhaite tester une méthode privée

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

Test unitaire Java avec Mockito
Comment faire un test unitaire Java (JUnit & Mockito & PowerMock)
Installez java avec Homebrew
Changer de siège avec Java
Installez Java avec Ansible
[Ralis] À propos du test unitaire
Téléchargement confortable avec JAVA
Échantillon de bibliothèque de tests unitaires Java
Changer java avec direnv
Téléchargement Java avec Ansible
Raclons avec Java! !!
Faites un test unitaire avec Junit.
Construire Java avec Wercker
Conversion Endian avec JAVA
Générer des données factices pour divers tests avec Faker (java)
(Java) BDD facile avec Spectrum?
Utiliser des couches Lambda avec Java
Créer un multi-projet Java avec Gradle
Premiers pas avec Java Collection
Configuration Java avec Spring MVC
Authentification de base avec Java 11 HttpClient
Expérimentons l'expansion en ligne Java
Exécuter un lot avec docker-compose avec Java batch
[Template] Connexion MySQL avec Java
Réécrire Java try-catch avec facultatif
Installez Java 7 avec Homebrew (cask)
[Java] Communication JSON avec jackson
Java pour jouer avec Function
Activer Java EE avec NetBeans 9
[Java] JavaConfig avec classe interne statique
Exploitons Excel avec Java! !!
Gestion des versions Java avec SDKMAN
Cryptage / décryptage RSA avec Java 8
Pagination de PDF avec Java + PDFBox.jar
Différents tests de méthode avec MockRestServiceServer
[Java] Acquisition de contenu avec HttpCliient
Gestion des versions Java avec jenv
Dépannage avec Java Flight Recorder
Connectez-vous à DB avec Java
Connectez-vous à MySQL 8 avec Java
Erreur lors de la lecture avec java
Premiers pas avec les bases de Java
Affichage saisonnier avec commutateur Java
Utiliser SpatiaLite avec Java / JDBC
Comparez Java 8 en option avec Swift
Exécuter Java VM avec Web Assembly
Utilisez Spring Test + Mockito + JUnit 4 pour le test unitaire Spring Boot + Spring Retry
Transition d'écran avec swing, java
[Java 8] Suppression en double (et vérification en double) avec Stream
Expression lambda Java apprise avec Comparator
Valider les arguments à l'aide d'ArgumentCaptor avec mockito
Construire un projet Java avec Gradle
Installer Java avec Docker basé sur Ubuntu 16.04
Java pour apprendre avec les ramen [Partie 1]
Analyse morphologique en Java avec Kuromoji
Utiliser java avec MSYS et Cygwin
Traçage distribué avec OpenCensus et Java
Java Unit Test Library-Artery-ArValidator valide les objets