Skip to Content
04 TestingTests Unitaires

Tests Unitaires

Les tests unitaires permettent de tester une seule classe de manière isolée, sans dépendances externes (pas de serveur, pas de base de données).

Les tests unitaires sont ultra rapides et testent la logique métier pure avec des mocks.


Qu’est-ce qu’un Test Unitaire ?

Un test unitaire vérifie le comportement d’une seule unité (classe, méthode) en isolation complète.

Caractéristiques

  • Rapide - Exécution en millisecondes
  • Isolé - Pas de serveur HTTP, pas de base de données
  • Ciblé - Teste une seule classe
  • Déterministe - Même résultat à chaque exécution
  • Indépendant - Ne dépend pas des autres tests

Différence avec Tests d’Intégration

Test Unitaire

// Pas d'annotation @MicronautTest class GetTrendsUseCaseTest { private TrendRepository mockRepository; private GetTrendsUseCase useCase; @BeforeEach void setUp() { // Créer un mock manuellement mockRepository = mock(TrendRepository.class); // Instancier la classe testée useCase = new GetTrendsUseCase(mockRepository); } @Test void shouldReturnTrends() { // Mock du comportement when(mockRepository.getTrends(any())) .thenReturn(Optional.of(mockData)); // Test rapide var result = useCase.execute("java"); assertThat(result).isPresent(); } }

Caractéristiques :

  • ❌ Pas de @MicronautTest
  • ❌ Pas d’injection automatique
  • ✅ Instanciation manuelle
  • ✅ Mocks avec Mockito
  • ✅ Exécution ultra rapide

Anatomie d’un Test Unitaire

Un test unitaire suit le pattern AAA : Arrange, Act, Assert.

Structure Standard

class GetTrendsUseCaseTest { // Dépendances mockées private TrendRepository mockRepository; private Logger mockLogger; // Classe testée private GetTrendsUseCase useCase; @BeforeEach void setUp() { // ARRANGE - Créer les mocks mockRepository = mock(TrendRepository.class); mockLogger = mock(Logger.class); // Instancier la classe testée avec ses dépendances useCase = new GetTrendsUseCase(mockRepository, mockLogger); } @Test @DisplayName("devrait retourner des tendances quand le mot-clé est valide") void shouldReturnTrendsWhenKeywordIsValid() { // ARRANGE - Préparer les données de test String keyword = "java"; TrendResult expectedResult = TrendResult.builder() .keyword(keyword) .interestScore(85) .build(); when(mockRepository.getTrends(any(TrendQuery.class))) .thenReturn(Optional.of(expectedResult)); // ACT - Exécuter la méthode testée Optional<TrendResult> result = useCase.execute(keyword, "US"); // ASSERT - Vérifier le résultat assertThat(result).isPresent(); assertThat(result.get().getKeyword()).isEqualTo("java"); assertThat(result.get().getInterestScore()).isEqualTo(85); // Vérifier que le mock a été appelé verify(mockRepository).getTrends(argThat(query -> query.getKeyword().equals("java") && query.getRegion().equals("US") )); } }

Mockito : Créer des Mocks

Mockito permet de créer des objets “faux” qui simulent le comportement des dépendances.

Créer un Mock

Méthode Simple (Recommandée)

class MyUseCaseTest { private MyRepository mockRepository; private MyUseCase useCase; @BeforeEach void setUp() { // Créer le mock manuellement mockRepository = mock(MyRepository.class); // Instancier la classe testée useCase = new MyUseCase(mockRepository); } @Test void myTest() { // Utiliser le mock } }

Simple et explicitePas d’annotation magiqueContrôle total


Définir le Comportement des Mocks

Utilisez when(...).thenReturn(...) pour définir ce que le mock doit retourner.

Retourner une Valeur

@Test void shouldReturnMockedValue() { // ARRANGE - Définir le comportement TrendResult mockResult = new TrendResult("java", 85); when(mockRepository.getTrends(any())) .thenReturn(Optional.of(mockResult)); // ACT Optional<TrendResult> result = useCase.execute("java"); // ASSERT assertThat(result).isPresent(); assertThat(result.get().getInterestScore()).isEqualTo(85); }

Lancer une Exception

@Test void shouldHandleRepositoryException() { // ARRANGE - Le mock lance une exception when(mockRepository.getTrends(any())) .thenThrow(new RuntimeException("Database connection failed")); // ACT & ASSERT assertThatThrownBy(() -> useCase.execute("java")) .isInstanceOf(RuntimeException.class) .hasMessage("Database connection failed"); }

Comportement selon les Arguments

@Test void shouldReturnDifferentResultsBasedOnKeyword() { // Comportement différent selon le keyword when(mockRepository.getTrends(argThat(q -> q.getKeyword().equals("java")))) .thenReturn(Optional.of(new TrendResult("java", 92))); when(mockRepository.getTrends(argThat(q -> q.getKeyword().equals("python")))) .thenReturn(Optional.of(new TrendResult("python", 88))); var javaResult = useCase.execute("java"); var pythonResult = useCase.execute("python"); assertThat(javaResult.get().getInterestScore()).isEqualTo(92); assertThat(pythonResult.get().getInterestScore()).isEqualTo(88); }

Vérifier les Appels (Verify)

Utilisez verify(...) pour vérifier que le mock a été appelé correctement.

Vérifier qu’une Méthode a Été Appelée

@Test void shouldCallRepository() { // ARRANGE when(mockRepository.getTrends(any())).thenReturn(Optional.empty()); // ACT useCase.execute("java"); // ASSERT - Vérifier l'appel verify(mockRepository).getTrends(any()); }

Vérifier le Nombre d’Appels

@Test void shouldCallRepositoryExactlyTwice() { when(mockRepository.getTrends(any())).thenReturn(Optional.empty()); useCase.execute("java"); useCase.execute("python"); // Vérifier exactement 2 appels verify(mockRepository, times(2)).getTrends(any()); }

Vérifier les Arguments

@Test void shouldCallWithCorrectArguments() { when(mockRepository.getTrends(any())).thenReturn(Optional.empty()); useCase.execute("java", "US"); // Vérifier que l'argument correspond verify(mockRepository).getTrends(argThat(query -> query.getKeyword().equals("java") && query.getRegion().equals("US") )); }

Assertions avec AssertJ

AssertJ offre une syntaxe fluide et lisible pour les assertions.

Assertions de Base

// Valeurs simples assertThat(result).isNotNull(); assertThat(result.getKeyword()).isEqualTo("java"); assertThat(result.getInterestScore()).isGreaterThan(0); assertThat(result.getInterestScore()).isBetween(1, 100); // Booléens assertThat(result.isPopular()).isTrue(); assertThat(result.isDeprecated()).isFalse(); // Strings assertThat(result.getKeyword()).startsWith("jav"); assertThat(result.getKeyword()).endsWith("va"); assertThat(result.getKeyword()).contains("av");

Assertions sur Optional

// Optional présent assertThat(result).isPresent(); assertThat(result).contains(expectedValue); // Optional vide assertThat(result).isEmpty(); assertThat(result).isNotPresent(); // Extraire la valeur assertThat(result) .isPresent() .get() .satisfies(trend -> { assertThat(trend.getKeyword()).isEqualTo("java"); assertThat(trend.getInterestScore()).isPositive(); });

Assertions sur Collections

List<String> topics = result.getRelatedTopics(); // Taille assertThat(topics).hasSize(4); assertThat(topics).isNotEmpty(); assertThat(topics).hasSizeGreaterThan(2); // Contenu assertThat(topics).contains("Spring", "Micronaut"); assertThat(topics).containsOnly("Spring", "Micronaut", "Quarkus"); assertThat(topics).doesNotContain("PHP"); // Extraction assertThat(topics) .extracting(String::toUpperCase) .containsExactly("SPRING", "MICRONAUT", "QUARKUS", "HELIDON");

Assertions sur Exceptions

// Vérifier qu'une exception est lancée assertThatThrownBy(() -> useCase.execute(null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Keyword cannot be null") .hasMessageContaining("cannot be null"); // Vérifier qu'aucune exception n'est lancée assertThatCode(() -> useCase.execute("java")) .doesNotThrowAnyException();

Exemple Complet : Test d’un Use Case

Exemple complet de test unitaire d’un use case avec plusieurs scénarios.

package org.smoka.application.usecase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.smoka.domain.model.TrendQuery; import org.smoka.domain.model.TrendResult; import org.smoka.domain.port.output.TrendRepository; import java.util.Optional; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; class GetTrendsUseCaseTest { private TrendRepository mockRepository; private GetTrendsUseCase useCase; @BeforeEach void setUp() { mockRepository = mock(TrendRepository.class); useCase = new GetTrendsUseCase(mockRepository); } @Test @DisplayName("devrait retourner des tendances quand le mot-clé existe") void shouldReturnTrendsWhenKeywordExists() { // ARRANGE TrendResult mockResult = TrendResult.builder() .keyword("java") .region("US") .interestScore(85) .popularityLevel("HIGH") .build(); when(mockRepository.getTrends(any(TrendQuery.class))) .thenReturn(Optional.of(mockResult)); // ACT Optional<TrendResult> result = useCase.execute("java", "US"); // ASSERT assertThat(result) .isPresent() .get() .satisfies(trend -> { assertThat(trend.getKeyword()).isEqualTo("java"); assertThat(trend.getRegion()).isEqualTo("US"); assertThat(trend.getInterestScore()).isEqualTo(85); assertThat(trend.getPopularityLevel()).isEqualTo("HIGH"); }); verify(mockRepository).getTrends(argThat(query -> query.getKeyword().equals("java") && query.getRegion().equals("US") )); } @Test @DisplayName("devrait retourner Optional.empty() quand aucune tendance trouvée") void shouldReturnEmptyWhenNoTrendFound() { // ARRANGE when(mockRepository.getTrends(any())) .thenReturn(Optional.empty()); // ACT Optional<TrendResult> result = useCase.execute("unknownKeyword", "ZZ"); // ASSERT assertThat(result).isEmpty(); verify(mockRepository, times(1)).getTrends(any()); } @Test @DisplayName("devrait lancer une exception quand le repository échoue") void shouldThrowExceptionWhenRepositoryFails() { // ARRANGE when(mockRepository.getTrends(any())) .thenThrow(new RuntimeException("Database connection failed")); // ACT & ASSERT assertThatThrownBy(() -> useCase.execute("java", "US")) .isInstanceOf(RuntimeException.class) .hasMessage("Database connection failed"); verify(mockRepository).getTrends(any()); } @Test @DisplayName("ne devrait jamais appeler le repository si keyword est null") void shouldNotCallRepositoryWhenKeywordIsNull() { // ACT & ASSERT assertThatThrownBy(() -> useCase.execute(null, "US")) .isInstanceOf(IllegalArgumentException.class); // Le repository ne devrait JAMAIS être appelé verify(mockRepository, never()).getTrends(any()); } }

Bonnes Pratiques

Suivez ces bonnes pratiques pour écrire des tests unitaires maintenables.

Nommage Explicite

@Test @DisplayName("devrait retourner 404 quand le mot-clé n'existe pas") void shouldReturn404WhenKeywordNotFound() { } @Test @DisplayName("devrait valider le format du mot-clé") void shouldValidateKeywordFormat() { } @Test @DisplayName("devrait gérer les caractères spéciaux") void shouldHandleSpecialCharacters() { }

Nom descriptifDisplayName expliciteConvention should/when

Un Test = Un Concept

// Bon - chaque test vérifie une chose @Test void shouldReturn200() { } @Test void shouldReturnCorrectKeyword() { } @Test void shouldReturnValidScore() { } // Mauvais - trop de vérifications @Test void shouldWorkCorrectly() { // vérifie 10 choses différentes }

Pattern AAA (Arrange, Act, Assert)

@Test void shouldReturnTrends() { // ARRANGE - Préparer les données String keyword = "java"; TrendResult mockResult = new TrendResult(keyword, 85); when(mockRepository.getTrends(any())).thenReturn(Optional.of(mockResult)); // ACT - Exécuter l'action Optional<TrendResult> result = useCase.execute(keyword); // ASSERT - Vérifier le résultat assertThat(result).isPresent(); assertThat(result.get().getKeyword()).isEqualTo("java"); }

Tester les Cas Limites

@Test void shouldHandleEmptyKeyword() { } @Test void shouldHandleNullKeyword() { } @Test void shouldHandleVeryLongKeyword() { } @Test void shouldHandleSpecialCharacters() { } @Test void shouldHandleUnicodeCharacters() { }

Éviter la Duplication

@BeforeEach void setUp() { // Code commun à tous les tests mockRepository = mock(TrendRepository.class); useCase = new GetTrendsUseCase(mockRepository); } // Ou créer des méthodes helper private TrendResult createMockTrend(String keyword, int score) { return TrendResult.builder() .keyword(keyword) .interestScore(score) .build(); }

Checklist Tests Unitaires

Pour vérifier que vos tests unitaires sont de qualité :

  • Pas de MicronautTest - Tests unitaires purs
  • Instanciation manuelle - Pas d’injection automatique
  • Mocks pour les dépendances - Mockito
  • Pattern AAA - Arrange, Act, Assert
  • Nommage explicite - Nom descriptif + DisplayName
  • Un test = un concept - Pas de test fourre-tout
  • Cas limites testés - null, empty, invalid, etc.
  • Vérification des appels - verify() pour les mocks
  • AssertJ - Assertions fluides et lisibles
  • Rapide - Exécution rapide

Résumé

Les tests unitaires sont essentiels pour valider la logique métier en isolation.

AspectTest Unitaire
AnnotationAucune (pas de MicronautTest)
VitesseUltra rapide
ScopeUne seule classe
DépendancesMockées avec Mockito
InstanciationManuelle
InjectionManuelle via constructeur
Quand utiliserLogique métier, use cases, calculs

Prochaines Étapes

Maintenant que vous maîtrisez les tests unitaires, découvrez les tests d’intégration.

Last updated on