Table des matières

Tests unitaires

On utilise souvent la matrice Given-When-Then (Structure AAA) :

// Given (Arrange) - Préparer les données
// When (Act) - Exécuter l'action 
// Then (Assert) - Vérifier le résultat

On isole le service à tester en utilisant des Mock objects (objets factices).

Vérification du WorkFlow métier :

Exemple :

Si la catégorie n'existe pas → le service ne doit PAS essayer de sauver le produit !

Bonnes pratiques

FIRST

Fast

Feeback rapide après lancement de la suite

Independant

Ne pas faire dépendre un test du résultat d'autres tests, ni de l'environnement

Repeatable

Un test doit toujours produire le même résultat, prédictible

Self-validating

Doit être autonome pour déterminer la validité de son résultat

Timely

Fait au bon moment (avant l'écriture du code de prod dans l'idéal)

Ressources

Annotations Spring/Mockito

@ExtendWith(MockitoExtension::class)  // Pour tests unitaires
@SpringBootTest                       // Application complète
@WebMvcTest(Controller::class)        // Tests controllers
@DataJpaTest                         // Tests repositories
@Mock                                // Faux objet
@MockBean                           // Mock dans contexte Spring
@InjectMocks                        // Vraie instance avec mocks injectés

Mocking concepts

Assertions usuelles

assertThat(result).isNotNull()
assertThat(result.name).isEqualTo("expected")
assertThat(result.price).isEqualByComparingTo(BigDecimal("10.50"))
assertThatThrownBy { ... }.isInstanceOf(Exception::class.java)

Exemple de test

@ExtendWith(MockitoExtension::class)
class ProductServiceTest {

    @Mock
    private lateinit var productRepository: ProductRepository
    
    @Mock
    private lateinit var categoryRepository: CategoryRepository
    
    @InjectMocks
    private lateinit var productService: ProductService

    @Test
    @DisplayName("Should create product successfully when category exists")
    fun `should create product when category exists`() {
        // Given
        val categoryId = UUID.fromString("550e8400-e29b-41d4-a716-446655440010")
        val productId = UUID.fromString("550e8400-e29b-41d4-a716-446655440020")
        
        val createDTO = CreateProductDTO(
            name = "iPhone 15",
            description = "Latest Apple smartphone",
            price = BigDecimal("1199.99"),
            stock = 50,
            categoryId = categoryId
        )
        
        val category = Category(categoryId, "Smartphones", "Mobile devices")
        val savedProduct = Product(productId, "iPhone 15", "Latest Apple smartphone", 
            BigDecimal("1199.99"), 50, category, ProductStatus.ACTIVE)
        
        `when`(categoryRepository.findById(categoryId)).thenReturn(Optional.of(category))
        `when`(productRepository.save(any(Product::class.java))).thenReturn(savedProduct)

        // When
        val result = productService.createProduct(createDTO)

        // Then
        assertThat(result).isNotNull
        assertThat(result.id).isEqualTo(productId)
        assertThat(result.name).isEqualTo("iPhone 15")
        assertThat(result.price).isEqualByComparingTo(BigDecimal("1199.99"))
        assertThat(result.stock).isEqualTo(50)
        
        verify(categoryRepository).findById(categoryId)
        verify(productRepository).save(any(Product::class.java))
    }

    @Test
    @DisplayName("Should throw exception when category not found")
    fun `should throw exception when category not found`() {
        // Given
        val categoryId = UUID.fromString("550e8400-e29b-41d4-a716-446655440099")
        val createDTO = CreateProductDTO("iPhone 15", "Description", BigDecimal("1199.99"), 50, categoryId)
        
        `when`(categoryRepository.findById(categoryId)).thenReturn(Optional.empty())

        // When & Then
        assertThatThrownBy { productService.createProduct(createDTO) }
            .isInstanceOf(CategoryNotFoundException::class.java)
            .hasMessageContaining(categoryId.toString())
        
        verify(categoryRepository).findById(categoryId)
        verifyNoInteractions(productRepository)
    }
}