eadl:bloc3:dev_av:td3

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentes Révision précédente
Prochaine révision
Révision précédente
eadl:bloc3:dev_av:td3 [2025/10/28 17:07] jcheroneadl:bloc3:dev_av:td3 [2025/11/09 16:36] (Version actuelle) jcheron
Ligne 1: Ligne 1:
-====== Séance 3 - Tests et CI/CD (4h) ======+====== 3 - Tests et CI/CD ======
  
 ===== Objectifs pédagogiques ===== ===== Objectifs pédagogiques =====
Ligne 15: Ligne 15:
   * Qui a terminé les associations Order/OrderItem/User ?   * Qui a terminé les associations Order/OrderItem/User ?
   * Qui a résolu des problèmes N+1 ?   * Qui a résolu des problèmes N+1 ?
-  * **Décision :** Ceux qui ont fini peuvent commencer les tests, les autres finalisent le TD2+  * Ceux qui ont fini peuvent commencer les tests, les autres finalisent le TD2
 </WRAP> </WRAP>
  
Ligne 26: Ligne 26:
 </WRAP> </WRAP>
  
-=== Structure des fichiers ===+=== Profiles ===
  
-<sxh;gutter:false+La création de profiles permet de gérer des configurations différentes, et des fichiers de configuration spécifiques à chaque profile. 
-src/main/resources+ 
-├── application.properties           # Configuration commune +=== pom.xml - Configuration des profils Maven === 
-├── application-dev.properties       # Développement local + 
-├── application-test.properties      # Tests automatisés +<sxh xml
-└── application-prod.properties      # Production+<profiles> 
 +    <profile> 
 +        <id>dev</id> 
 +        <activation> 
 +            <activeByDefault>true</activeByDefault> 
 +        </activation> 
 +        <properties> 
 +            <activeProfile>dev</activeProfile> 
 +        </properties> 
 +    </profile> 
 +     
 +    <profile> 
 +        <id>test</id> 
 +        <properties> 
 +            <activeProfile>test</activeProfile> 
 +        </properties> 
 +    </profile> 
 +     
 +    <profile> 
 +        <id>prod</id> 
 +        <properties
 +            <activeProfile>prod</activeProfile> 
 +        </properties> 
 +    </profile> 
 +</profiles>
 </sxh> </sxh>
  
 === application.properties (commun) === === application.properties (commun) ===
  
-<sxh properties;gutter:false+<sxh properties> 
-Configuration commune à tous les profils +Récupération du profile Maven pour def du profile Spring 
-spring.application.name=ecommerce-api +spring.profiles.active=@activeProfile@
-server.port=8080+
  
 # JPA commun # JPA commun
Ligne 53: Ligne 76:
 === application-dev.properties === === application-dev.properties ===
  
-<sxh properties;gutter:false>+<sxh properties>
 # Base H2 fichier pour le dev # Base H2 fichier pour le dev
 spring.datasource.url=jdbc:h2:file:./data/ecommerce-dev spring.datasource.url=jdbc:h2:file:./data/ecommerce-dev
Ligne 75: Ligne 98:
 === application-test.properties === === application-test.properties ===
  
-<sxh properties;gutter:false>+<sxh properties>
 # Base H2 en mémoire pour les tests # Base H2 en mémoire pour les tests
 spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1 spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1
Ligne 98: Ligne 121:
 === application-prod.properties === === application-prod.properties ===
  
-<sxh properties;gutter:false>+<sxh properties>
 # Base PostgreSQL (exemple) # Base PostgreSQL (exemple)
 spring.datasource.url=${DATABASE_URL} spring.datasource.url=${DATABASE_URL}
Ligne 117: Ligne 140:
 ==== 1.2 Activation des profils ==== ==== 1.2 Activation des profils ====
  
-<sxh bash;gutter:false>+<sxh bash>
 # Dans IntelliJ : Run Configuration > Active profiles: dev # Dans IntelliJ : Run Configuration > Active profiles: dev
 # Ou via variable d'environnement # Ou via variable d'environnement
Ligne 124: Ligne 147:
 # Via ligne de commande # Via ligne de commande
 mvn spring-boot:run -Dspring-boot.run.profiles=dev mvn spring-boot:run -Dspring-boot.run.profiles=dev
 +
 +# Avec profil maven
 +mvn spring-boot:run -P dev
 </sxh> </sxh>
  
Ligne 148: Ligne 174:
         <groupId>org.springframework.boot</groupId>         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>         <artifactId>spring-boot-starter-test</artifactId>
 +        <scope>test</scope>
 +    </dependency>
 +    
 +    <!-- MockK pour Kotlin (optionnel, alternative à Mockito) -->
 +    <dependency>
 +        <groupId>com.ninja-squad</groupId>
 +        <artifactId>springmockk</artifactId>
 +        <version>4.0.2</version>
         <scope>test</scope>         <scope>test</scope>
     </dependency>     </dependency>
Ligne 155: Ligne 189:
 ==== 2.2 Premier test simple : ProductService ==== ==== 2.2 Premier test simple : ProductService ====
  
-<sxh java+<sxh kotlin
-package com.ecommerce.service+package com.ecommerce.service
- +
-import com.ecommerce.domain.Product; +
-import com.ecommerce.domain.Category; +
-import com.ecommerce.repository.ProductRepository; +
-import com.ecommerce.exception.ProductNotFoundException; +
-import com.ecommerce.exception.InsufficientStockException; +
-import org.junit.jupiter.api.Test; +
-import org.junit.jupiter.api.DisplayName; +
-import org.junit.jupiter.api.extension.ExtendWith; +
-import org.mockito.InjectMocks; +
-import org.mockito.Mock; +
-import org.mockito.junit.jupiter.MockitoExtension; +
- +
-import java.math.BigDecimal; +
-import java.util.Optional; +
-import java.util.UUID;+
  
-import static org.assertj.core.api.Assertions.*; +import com.ecommerce.domain.Product 
-import static org.mockito.Mockito.*;+import com.ecommerce.domain.Category 
 +import com.ecommerce.repository.ProductRepository 
 +import com.ecommerce.exception.ProductNotFoundException 
 +import com.ecommerce.exception.InsufficientStockException 
 +import io.mockk.every 
 +import io.mockk.mockk 
 +import io.mockk.verify 
 +import io.mockk.slot 
 +import org.assertj.core.api.Assertions.* 
 +import org.junit.jupiter.api.Test 
 +import org.junit.jupiter.api.DisplayName 
 +import org.junit.jupiter.api.BeforeEach 
 +import java.math.BigDecimal 
 +import java.util.*
  
-@ExtendWith(MockitoExtension.class) 
 @DisplayName("ProductService - Unit Tests") @DisplayName("ProductService - Unit Tests")
 class ProductServiceTest { class ProductServiceTest {
  
-    @Mock +    private lateinit var productRepository: ProductRepository 
-    private ProductRepository productRepository;+    private lateinit var productService: ProductService 
 +    private lateinit var category: Category
  
-    @InjectMocks +    @BeforeEach 
-    private ProductService productService;+    fun setUp() { 
 +        productRepository = mockk() 
 +        productService = ProductService(productRepository) 
 +        category = Category(name = "Electronics", description = "Electronic products"
 +        category.id = UUID.randomUUID() 
 +    }
  
     @Test     @Test
-    @DisplayName("Should return product when it exists") +    @DisplayName("Should create product with valid data") 
-    void getProduct_WhenExists_ShouldReturnProduct() { +    fun `createProduct with valid data should return product`() { 
-        // Given (Arrange) +        // Given 
-        UUID productId UUID.randomUUID(); +        val productSlot slot<Product>() 
-        Category category new Category("Electronics", "Devices"); +        val savedProduct = Product( 
-        Product expectedProduct = Product.builder() +            name "iPhone 15", 
-            .id(productId) +            price BigDecimal("999.99"), 
-            .name("iPhone") +            stock 10, 
-            .price(new BigDecimal("999.99")) +            category category 
-            .stock(10) +        ).apply { id = UUID.randomUUID() }
-            .category(category+
-            .build(); +
-         +
-        when(productRepository.findById(productId)+
-            .thenReturn(Optional.of(expectedProduct));+
  
-        // When (Act) +        every { productRepository.save(capture(productSlot)} returns savedProduct
-        Product result = productService.getProduct(productId);+
  
-        // Then (Assert+        // When 
-        assertThat(result).isNotNull(); +        val result = productService.createProduct( 
-        assertThat(result.getName()).isEqualTo("iPhone"); +            name = "iPhone 15", 
-        assertThat(result.getPrice()).isEqualByComparingTo("999.99"); +            price = BigDecimal("999.99")
-        assertThat(result.getStock()).isEqualTo(10)+            stock = 10, 
-         +            categoryId = category.id!! 
-        verify(productRepository, times(1)).findById(productId)+        ) 
-        verifyNoMoreInteractions(productRepository);+ 
 +        // Then 
 +        assertThat(result).isNotNull 
 +        assertThat(result.name).isEqualTo("iPhone 15") 
 +        assertThat(result.price).isEqualByComparingTo("999.99"
 +        assertThat(result.stock).isEqualTo(10) 
 +        verify(exactly = 1) { productRepository.save(any()) }
     }     }
  
     @Test     @Test
     @DisplayName("Should throw exception when product not found")     @DisplayName("Should throw exception when product not found")
-    void getProduct_WhenNotExists_ShouldThrowException() {+    fun `getProduct when not exists should throw exception`() {
         // Given         // Given
-        UUID productId = UUID.randomUUID(); +        val productId = UUID.randomUUID() 
-        when(productRepository.findById(productId)+        every { productRepository.findById(productId) } returns Optional.empty()
-            .thenReturn(Optional.empty());+
  
         // When & Then         // When & Then
-        assertThatThrownBy(() -> productService.getProduct(productId)) +        assertThatThrownBy productService.getProduct(productId) } 
-            .isInstanceOf(ProductNotFoundException.class+            .isInstanceOf(ProductNotFoundException::class.java
-            .hasMessageContaining(productId.toString())+            .hasMessageContaining(productId.toString())
-         +
-        verify(productRepository).findById(productId);+
     }     }
  
     @Test     @Test
-    @DisplayName("Should decrease stock when updating with negative quantity") +    @DisplayName("Should decrease stock when sufficient") 
-    void updateStock_WithNegativeQuantity_ShouldDecreaseStock() {+    fun `decreaseStock with sufficient stock should update product`() {
         // Given         // Given
-        UUID productId = UUID.randomUUID(); +        val productId = UUID.randomUUID() 
-        Product product = Product.builder() +        val product = Product( 
-            .id(productId) +            name "Test Product", 
-            .name("Test Product") +            price = BigDecimal("10.00"), 
-            .price(BigDecimal.TEN+            stock 10, 
-            .stock(10) +            category = category 
-            .build(); +        ).apply { id = productId } 
-         + 
-        when(productRepository.findById(productId)+        every { productRepository.findById(productId) } returns Optional.of(product) 
-            .thenReturn(Optional.of(product)); +        every { productRepository.save(any()) } returns product
-        when(productRepository.save(any(Product.class))+
-            .thenReturn(product);+
  
         // When         // When
-        productService.updateStock(productId, -3);+        productService.decreaseStock(productId, 3)
  
         // Then         // Then
-        assertThat(product.getStock()).isEqualTo(7); +        assertThat(product.stock).isEqualTo(7) 
-        verify(productRepository).save(product);+        verify(exactly = 1) { productRepository.save(product) }
     }     }
  
     @Test     @Test
     @DisplayName("Should throw exception when insufficient stock")     @DisplayName("Should throw exception when insufficient stock")
-    void updateStock_WithInsufficientStock_ShouldThrowException() {+    fun `decreaseStock with insufficient stock should throw exception`() {
         // Given         // Given
-        UUID productId = UUID.randomUUID(); +        val productId = UUID.randomUUID() 
-        Product product = Product.builder() +        val product = Product( 
-            .id(productId) +            name "Test Product", 
-            .name("Test Product") +            price BigDecimal("10.00"), 
-            .price(BigDecimal.TEN+            stock = 2, 
-            .stock(5) +            category = category 
-            .build(); +        ).apply { id = productId } 
-         + 
-        when(productRepository.findById(productId)+        every { productRepository.findById(productId) } returns Optional.of(product)
-            .thenReturn(Optional.of(product));+
  
         // When & Then         // When & Then
-        assertThatThrownBy(() -> productService.updateStock(productId, -10)+        assertThatThrownBy productService.decreaseStock(productId, 5} 
-            .isInstanceOf(InsufficientStockException.class); +            .isInstanceOf(InsufficientStockException::class.java
-         +            .hasMessageContaining("Insufficient stock")
-        verify(productRepository, never()).save(any());+
     }     }
  
     @Test     @Test
-    @DisplayName("Should increase stock when updating with positive quantity") +    @DisplayName("Should update stock correctly") 
-    void updateStock_WithPositiveQuantity_ShouldIncreaseStock() {+    fun `updateStock should change product stock and save`() {
         // Given         // Given
-        UUID productId = UUID.randomUUID(); +        val productId = UUID.randomUUID() 
-        Product product = Product.builder() +        val product = Product( 
-            .id(productId) +            name "Test Product", 
-            .name("Test Product") +            price = BigDecimal("10.00"), 
-            .price(BigDecimal.TEN+            stock 10, 
-            .stock(10) +            category = category 
-            .build(); +        ).apply { id = productId } 
-         + 
-        when(productRepository.findById(productId)+        every { productRepository.findById(productId) } returns Optional.of(product) 
-            .thenReturn(Optional.of(product)); +        every { productRepository.save(any()) } returns product
-        when(productRepository.save(any(Product.class))+
-            .thenReturn(product);+
  
         // When         // When
-        productService.updateStock(productId, 5);+        productService.updateStock(productId, 5)
  
         // Then         // Then
-        assertThat(product.getStock()).isEqualTo(15); +        assertThat(product.stock).isEqualTo(15) 
-        verify(productRepository).save(product);+        verify(exactly = 1) { productRepository.save(product) }
     }     }
 } }
Ligne 309: Ligne 336:
 ==== 2.3 Concepts clés ==== ==== 2.3 Concepts clés ====
  
-<sxh java;gutter:false+<sxh kotlin
-// @Mock : Crée un faux objet (ne fait rien par défaut+// MockK pour Kotlin (alternative à Mockito
-@Mock +import io.mockk.*
-private ProductRepository productRepository;+
  
-// @InjectMocks : Injecte automatiquement les mocks dans la classe testée +// Créer un mock 
-@InjectMocks +val repository = mockk<ProductRepository>()
-private ProductService productService;+
  
-// when(...).thenReturn(...) : Définit le comportement du mock +// Définir le comportement du mock 
-when(productRepository.findById(id)).thenReturn(Optional.of(product));+every { repository.findById(id) } returns Optional.of(product) 
 +every { repository.save(any()) } returns product
  
-// verify(...) : Vérifie qu'une méthode a été appelée (et combien de fois+// Capturer un argument 
-verify(productRepository, times(1)).save(any()); +val productSlot = slot<Product>() 
-verify(productRepository, never()).delete(any());+every { repository.save(capture(productSlot)) } returns product
  
-// assertThat(...) : Vérifie le résultat (AssertJ - plus lisible que assertEquals+// Vérifier qu'une méthode a été appelée 
-assertThat(result.getStock()).isEqualTo(7); +verify(exactly = 1) { repository.save(any()) } 
-assertThat(result).isNotNull(); +verify(atLeast = 1) { repository.findById(any()) } 
-assertThat(list).hasSize(3);+verify(exactly = 0{ repository.delete(any()) }
  
-// assertThatThrownBy : Vérifie qu'une exception est levée +// AssertJ - Assertions plus lisibles 
-assertThatThrownBy(() -> service.doSomething()) +assertThat(result.stock).isEqualTo(7) 
-    .isInstanceOf(MyException.class) +assertThat(result).isNotNull() 
-    .hasMessage("Expected message");+assertThat(list).hasSize(3) 
 +assertThat(price).isEqualByComparingTo("999.99"
 + 
 +// Vérifier qu'une exception est levée 
 +assertThatThrownBy service.doSomething() } 
 +    .isInstanceOf(MyException::class.java
 +    .hasMessage("Expected message") 
 +    .hasMessageContaining("partial")
 </sxh> </sxh>
  
 ==== 2.4 Tests paramétrés (en plus) ==== ==== 2.4 Tests paramétrés (en plus) ====
  
-<sxh java+<sxh kotlin
-import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.ParameterizedTest 
-import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.CsvSource 
-import org.junit.jupiter.params.provider.ValueSource;+import org.junit.jupiter.params.provider.ValueSource
  
-@ParameterizedTest +class ProductValidationTest {
-@DisplayName("Should validate price is positive"+
-@ValueSource(strings = {"-10.00", "-0.01", "0.00"}) +
-void createProduct_WithInvalidPrice_ShouldThrowException(String price) { +
-    // Given +
-    CreateProductDto dto = new CreateProductDto(); +
-    dto.setName("Test"); +
-    dto.setPrice(new BigDecimal(price)); +
-    dto.setStock(10);+
  
-    // When & Then +    @ParameterizedTest 
-    assertThatThrownBy(() -> productService.createProduct(dto)+    @DisplayName("Should validate price is positive") 
-        .isInstanceOf(InvalidPriceException.class)+    @ValueSource(strings = ["-10.00", "-0.01", "0.00"]) 
-}+    fun `createProduct with invalid price should throw exception`(price: String{ 
 +        // Given 
 +        val productService = ProductService(mockk())
  
-@ParameterizedTest +        // When & Then 
-@DisplayName("Should calculate correct total for different quantities"+        assertThatThrownBy 
-@CsvSource(+            productService.createProduct( 
-    "1, 10.00, 10.00", +                name = "Test", 
-    "2, 10.00, 20.00", +                price = BigDecimal(price), 
-    "5, 9.99, 49.95" +                stock = 10
-}) +                categoryId UUID.randomUUID() 
-void calculateTotal_WithDifferentQuantities_ShouldReturnCorrectAmount( +            
-    int quantity,  +        }.isInstanceOf(InvalidPriceException::class.java) 
-    String unitPrice,  +    }
-    String expectedTotal +
-) { +
-    // Given +
-    Product product Product.builder() +
-        .price(new BigDecimal(unitPrice)+
-        .build();+
  
-    // When +    @ParameterizedTest 
-    BigDecimal total = productService.calculateTotal(product, quantity);+    @DisplayName("Should calculate correct total for different quantities"
 +    @CsvSource( 
 +        "1, 10.00, 10.00", 
 +        "2, 10.00, 20.00", 
 +        "5, 9.99, 49.95" 
 +    ) 
 +    fun `calculateTotal with different quantities should return correct amount`( 
 +        quantity: Int, 
 +        unitPrice: String, 
 +        expectedTotal: String 
 +    ) { 
 +        // Given 
 +        val product = Product( 
 +            name = "Test", 
 +            price = BigDecimal(unitPrice), 
 +            stock = 100, 
 +            category = mockk() 
 +        ) 
 +        val productService = ProductService(mockk())
  
-    // Then +        // When 
-    assertThat(total).isEqualByComparingTo(expectedTotal);+        val total = productService.calculateTotal(product, quantity) 
 + 
 +        // Then 
 +        assertThat(total).isEqualByComparingTo(expectedTotal) 
 +    }
 } }
 </sxh> </sxh>
Ligne 394: Ligne 437:
  
 **Template fourni :** **Template fourni :**
-<code java> +<sxh kotlin>
-@ExtendWith(MockitoExtension.class)+
 @DisplayName("UserService - Unit Tests") @DisplayName("UserService - Unit Tests")
 class UserServiceTest { class UserServiceTest {
          
-    @Mock +    private lateinit var userRepository: UserRepository 
-    private UserRepository userRepository;+    private lateinit var orderRepository: OrderRepository 
 +    private lateinit var userService: UserService
          
-    @Mock +    @BeforeEach 
-    private OrderRepository orderRepository; +    fun setUp() { 
-     +        userRepository = mockk() 
-    @InjectMocks +        orderRepository = mockk() 
-    private UserService userService;+        userService = UserService(userRepository, orderRepository) 
 +    }
          
     @Test     @Test
     @DisplayName("Should create user with valid data")     @DisplayName("Should create user with valid data")
-    void createUser_WithValidData_ShouldReturnUser() {+    fun `createUser with valid data should return user`() {
         // Given         // Given
-        CreateUserDto dto new CreateUserDto("John Doe", "john@example.com"); +        val email = "john@example.com" 
-        User user = User.builder() +        val user = User(name = "John Doe", email = email).apply { 
-            .id(UUID.randomUUID()+            id UUID.randomUUID() 
-            .name(dto.getName()) +        }
-            .email(dto.getEmail()) +
-            .build();+
                  
-        when(userRepository.existsByEmail(dto.getEmail())).thenReturn(false); +        every { userRepository.existsByEmail(email} returns false 
-        when(userRepository.save(any(User.class))).thenReturn(user);+        every { userRepository.save(any()) } returns user
  
         // When         // When
-        User result = userService.createUser(dto);+        val result = userService.createUser(name = "John Doe", email = email)
  
         // Then         // Then
-        assertThat(result).isNotNull(); +        assertThat(result).isNotNull() 
-        assertThat(result.getEmail()).isEqualTo("john@example.com"); +        assertThat(result.email).isEqualTo(email
-        verify(userRepository).save(any(User.class));+        verify(exactly = 1) { userRepository.save(any()) }
     }     }
          
     // TODO: Implémenter les 4 autres tests     // TODO: Implémenter les 4 autres tests
 } }
-</code>+</sxh>
  
 **Critères de validation :** **Critères de validation :**
Ligne 450: Ligne 492:
 ==== 3.1 Configuration de base ==== ==== 3.1 Configuration de base ====
  
-<sxh java+<sxh kotlin
-package com.ecommerce.controller;+package com.ecommerce.controller
  
-import com.ecommerce.domain.Product; +import com.ecommerce.domain.Product 
-import com.ecommerce.domain.Category; +import com.ecommerce.domain.Category 
-import com.ecommerce.repository.ProductRepository; +import com.ecommerce.repository.ProductRepository 
-import com.ecommerce.repository.CategoryRepository; +import com.ecommerce.repository.CategoryRepository 
-import org.junit.jupiter.api.BeforeEach; +import org.hamcrest.Matchers.* 
-import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach 
-import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test 
-import org.springframework.beans.factory.annotation.Autowired; +import org.junit.jupiter.api.DisplayName 
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.beans.factory.annotation.Autowired 
-import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc 
-import org.springframework.http.MediaType; +import org.springframework.boot.test.context.SpringBootTest 
-import org.springframework.test.context.ActiveProfiles; +import org.springframework.http.MediaType 
-import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.context.ActiveProfiles 
-import org.springframework.transaction.annotation.Transactional+import org.springframework.test.web.servlet.* 
- +import org.springframework.transaction.annotation.Transactional 
-import java.math.BigDecimal+import java.math.BigDecimal
- +
-import static org.hamcrest.Matchers.*; +
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +
-import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;+
  
 @SpringBootTest @SpringBootTest
 @AutoConfigureMockMvc @AutoConfigureMockMvc
 @ActiveProfiles("test") @ActiveProfiles("test")
-@Transactional // Rollback automatique après chaque test+@Transactional
 @DisplayName("ProductController - Integration Tests") @DisplayName("ProductController - Integration Tests")
 class ProductControllerIntegrationTest { class ProductControllerIntegrationTest {
  
     @Autowired     @Autowired
-    private MockMvc mockMvc;+    private lateinit var mockMvc: MockMvc
  
     @Autowired     @Autowired
-    private ProductRepository productRepository;+    private lateinit var productRepository: ProductRepository
  
     @Autowired     @Autowired
-    private CategoryRepository categoryRepository;+    private lateinit var categoryRepository: CategoryRepository
  
-    private Category electronics;+    private lateinit var electronics: Category
  
     @BeforeEach     @BeforeEach
-    void setUp() { +    fun setUp() { 
-        // Nettoyage (si @Transactional ne suffit pas) +        productRepository.deleteAll() 
-        productRepository.deleteAll(); +        categoryRepository.deleteAll()
-        categoryRepository.deleteAll();+
                  
-        // Données de test 
         electronics = categoryRepository.save(         electronics = categoryRepository.save(
-            new Category("Electronics", "Electronic devices") +            Category( 
-        );+                name = "Electronics", 
 +                description = "Electronic products" 
 +            
 +        )
     }     }
  
     @Test     @Test
-    @DisplayName("GET /products/{id} should return 200 when product exists") +    @DisplayName("POST /products should create a new product"
-    void getProduct_WhenExists_ShouldReturn200() throws Exception {+    fun `createProduct should return 201 with created product`() {
         // Given         // Given
-        Product product productRepository.save( +        val requestBody """ 
-            Product.builder() +            { 
-                .name("iPhone 15") +                "name": "iPhone 15", 
-                .price(new BigDecimal("999.99")) +                "price"999.99, 
-                .stock(50) +                "stock": 10, 
-                .category(electronics) +                "categoryId": "${electronics.id}" 
-                .build() +            } 
-        );+        """.trimIndent()
  
         // When & Then         // When & Then
-        mockMvc.perform(get("/products/{id}", product.getId())+        mockMvc.post("/products"{ 
-            .andDo(print()) // Affiche la requête/réponse (utile pour déboguer) +            contentType = MediaType.APPLICATION_JSON 
-            .andExpect(status().isOk()) +            content = requestBody 
-            .andExpect(content().contentType(MediaType.APPLICATION_JSON)) +        }.andExpect { 
-            .andExpect(jsonPath("$.id").value(product.getId().toString())+            status { isCreated() } 
-            .andExpect(jsonPath("$.name").value("iPhone 15")) +            jsonPath("$.name"value("iPhone 15"} 
-            .andExpect(jsonPath("$.price").value(999.99)) +            jsonPath("$.price"value(999.99) } 
-            .andExpect(jsonPath("$.stock").value(50)+            jsonPath("$.stock"value(10} 
-            .andExpect(jsonPath("$.category.name").value("Electronics"));+            jsonPath("$.category.name"value("Electronics"
 +        }
     }     }
  
     @Test     @Test
-    @DisplayName("GET /products/{id} should return 404 when product not found") +    @DisplayName("GET /products/{id} should return product when exists") 
-    void getProduct_WhenNotExists_ShouldReturn404() throws Exception {+    fun `getProduct when exists should return 200 with product`() { 
 +        // Given 
 +        val product = productRepository.save( 
 +            Product( 
 +                name = "MacBook Pro", 
 +                price = BigDecimal("1999.99"), 
 +                stock = 5, 
 +                category = electronics 
 +            ) 
 +        ) 
         // When & Then         // When & Then
-        mockMvc.perform(get("/products/{id}", "00000000-0000-0000-0000-000000000000")+        mockMvc.get("/products/${product.id}"
-            .andExpect(status().isNotFound()) +            .andExpect 
-            .andExpect(jsonPath("$.message").exists());+                status { isOk() 
 +                jsonPath("$.id") { value(product.id.toString()) } 
 +                jsonPath("$.name") { value("MacBook Pro") } 
 +                jsonPath("$.price"{ value(1999.99
 +            }
     }     }
  
     @Test     @Test
-    @DisplayName("POST /products should return 201 with valid data") +    @DisplayName("GET /products/{id} should return 404 when not found") 
-    void createProduct_WithValidData_ShouldReturn201() throws Exception {+    fun `getProduct when not exists should return 404`() {
         // Given         // Given
-        String requestBody """ +        val nonExistentId java.util.UUID.randomUUID()
-            { +
-                "name": "iPad Pro", +
-                "price": 799.99, +
-                "stock": 30, +
-                "categoryId": "%s" +
-            } +
-            """.formatted(electronics.getId());+
  
         // When & Then         // When & Then
-        mockMvc.perform(post("/products") +        mockMvc.get("/products/$nonExistentId") 
-                .contentType(MediaType.APPLICATION_JSON) +            .andExpect { 
-                .content(requestBody)) +                status { isNotFound() } 
-            .andDo(print()) +            }
-            .andExpect(status().isCreated()) +
-            .andExpect(header().exists("Location")) +
-            .andExpect(jsonPath("$.id").exists()+
-            .andExpect(jsonPath("$.name").value("iPad Pro")) +
-            .andExpect(jsonPath("$.price").value(799.99)+
-            .andExpect(jsonPath("$.stock").value(30));+
     }     }
  
     @Test     @Test
-    @DisplayName("POST /products should return 400 with invalid data") +    @DisplayName("PUT /products/{id} should update product") 
-    void createProduct_WithInvalidData_ShouldReturn400() throws Exception +    fun `updateProduct should return 200 with updated product`() { 
-        // Given - prix négatif +        // Given 
-        String requestBody = """+        val product = productRepository.save( 
 +            Product( 
 +                name = "iPad", 
 +                price = BigDecimal("699.99"), 
 +                stock = 15, 
 +                category = electronics 
 +            ) 
 +        ) 
 + 
 +        val updateRequest = """
             {             {
-                "name": "Invalid Product", +                "name": "iPad Pro", 
-                "price": -10.00+                "price": 899.99
-                "stock": 10, +                "stock": 20
-                "categoryId": "%s"+
             }             }
-            """.formatted(electronics.getId());+        """.trimIndent()
  
         // When & Then         // When & Then
-        mockMvc.perform(post("/products"+        mockMvc.put("/products/${product.id}"{ 
-                .contentType(MediaType.APPLICATION_JSON) +            contentType MediaType.APPLICATION_JSON 
-                .content(requestBody)) +            content = updateRequest 
-            .andExpect(status().isBadRequest()) +        }.andExpect 
-            .andExpect(jsonPath("$.errors").isArray());+            status { isOk() 
 +            jsonPath("$.name") { value("iPad Pro"} 
 +            jsonPath("$.price") { value(899.99) } 
 +            jsonPath("$.stock"{ value(20
 +        }
     }     }
  
     @Test     @Test
-    @DisplayName("PUT /products/{id}/stock should update stock correctly") +    @DisplayName("DELETE /products/{id} should delete product") 
-    void updateStock_WithValidQuantity_ShouldReturn200() throws Exception {+    fun `deleteProduct should return 204`() {
         // Given         // Given
-        Product product = productRepository.save( +        val product = productRepository.save( 
-            Product.builder() +            Product( 
-                .name("Test Product") +                name "AirPods", 
-                .price(BigDecimal.TEN+                price BigDecimal("199.99"), 
-                .stock(10) +                stock = 50, 
-                .category(electronics) +                category electronics 
-                .build(+            
-        );+        )
  
-        String requestBody = """ +        // When & Then 
-            { +        mockMvc.delete("/products/${product.id}") 
-                "quantity": 5+            .andExpect 
 +                status { isNoContent() }
             }             }
-            """; 
  
-        // When & Then +        // Verify deletion 
-        mockMvc.perform(put("/products/{id}/stock", product.getId()+        mockMvc.get("/products/${product.id}"
-                .contentType(MediaType.APPLICATION_JSON) +            .andExpect { 
-                .content(requestBody)) +                status { isNotFound() } 
-            .andExpect(status().isOk()) +            }
-            .andExpect(jsonPath("$.stock").value(15));+
     }     }
  
     @Test     @Test
     @DisplayName("GET /products should return paginated list")     @DisplayName("GET /products should return paginated list")
-    void getProducts_ShouldReturnPaginatedList() throws Exception {+    fun `getProducts should return paginated results`() {
         // Given         // Given
-        productRepository.save(Product.builder() +        productRepository.save( 
-            .name("Product 1") +            Product( 
-            .price(BigDecimal.TEN+                name "iPhone", 
-            .stock(10) +                price = BigDecimal("999"), 
-            .category(electronics) +                stock 10, 
-            .build()); +                category electronics 
-         +            ) 
-        productRepository.save(Product.builder() +        ) 
-            .name("Product 2") +        productRepository.save( 
-            .price(BigDecimal.valueOf(20)+            Product( 
-            .stock(20) +                name "MacBook", 
-            .category(electronics) +                price BigDecimal("1999"), 
-            .build());+                stock = 5, 
 +                category electronics 
 +            ) 
 +        )
  
         // When & Then         // When & Then
-        mockMvc.perform(get("/products"+        mockMvc.get("/products"{ 
-                .param("page", "0"+            param("page", "0"
-                .param("size", "10")+            param("size", "10"
-            .andExpect(status().isOk()) +        }.andExpect 
-            .andExpect(jsonPath("$.content").isArray()) +            status isOk() } 
-            .andExpect(jsonPath("$.content", hasSize(2))) +            jsonPath("$.content"isArray() } 
-            .andExpect(jsonPath("$.totalElements").value(2));+            jsonPath("$.content.length()") { value(2) } 
 +            jsonPath("$.totalElements"value(2) 
 +        }
     }     }
  
     @Test     @Test
     @DisplayName("GET /products should filter by category")     @DisplayName("GET /products should filter by category")
-    void getProducts_WithCategoryFilter_ShouldReturnFilteredList() throws Exception {+    fun `getProducts with category filter should return filtered list`() {
         // Given         // Given
-        Category books = categoryRepository.save(new Category("Books", "Books category"));+        val books = categoryRepository.save( 
 +            Category("Books", "Books category") 
 +        )
                  
-        productRepository.save(Product.builder() +        productRepository.save( 
-            .name("iPhone") +            Product( 
-            .price(BigDecimal.valueOf(999)) +                name "iPhone", 
-            .stock(10) +                price BigDecimal("999"), 
-            .category(electronics) +                stock 10, 
-            .build());+                category electronics 
 +            ) 
 +        )
                  
-        productRepository.save(Product.builder() +        productRepository.save( 
-            .name("Java Book") +            Product( 
-            .price(BigDecimal.valueOf(50)) +                name "Java Book", 
-            .stock(20) +                price BigDecimal("50"), 
-            .category(books) +                stock 20, 
-            .build());+                category books 
 +            ) 
 +        )
  
         // When & Then         // When & Then
-        mockMvc.perform(get("/products"+        mockMvc.get("/products"{ 
-                .param("categoryId", electronics.getId().toString())) +            param("categoryId", electronics.id.toString()) 
-            .andExpect(status().isOk()) +        }.andExpect 
-            .andExpect(jsonPath("$.content", hasSize(1))) +            status isOk() } 
-            .andExpect(jsonPath("$.content[0].name").value("iPhone"));+            jsonPath("$.content.length()") { value(1) } 
 +            jsonPath("$.content[0].name"value("iPhone"
 +        }
     }     }
 } }
Ligne 673: Ligne 737:
 ==== 3.2 Concepts clés ==== ==== 3.2 Concepts clés ====
  
-<sxh java;gutter:false>+<sxh kotlin>
 // @SpringBootTest : Lance toute l'application Spring // @SpringBootTest : Lance toute l'application Spring
 @SpringBootTest @SpringBootTest
Ligne 686: Ligne 750:
 @Transactional @Transactional
  
-// MockMvc : Simule des requêtes HTTP sans démarrer le serveur +// MockMvc DSL Kotlin : Simule des requêtes HTTP sans démarrer le serveur 
-mockMvc.perform(get("/products/123")+mockMvc.get("/products/123"
-    .andExpect(status().isOk()) +    .andExpect 
-    .andExpect(jsonPath("$.name").value("iPhone"));+        status isOk() } 
 +        jsonPath("$.name"value("iPhone"
 +    } 
 + 
 +mockMvc.post("/products"
 +    contentType = MediaType.APPLICATION_JSON 
 +    content = jsonBody 
 +}.andExpect { 
 +    status { isCreated() } 
 +}
  
 // jsonPath : Parcourt la réponse JSON avec des expressions // jsonPath : Parcourt la réponse JSON avec des expressions
Ligne 695: Ligne 768:
 jsonPath("$.category.name"    // Objet imbriqué jsonPath("$.category.name"    // Objet imbriqué
 jsonPath("$.items[0].name"    // Premier élément d'un tableau jsonPath("$.items[0].name"    // Premier élément d'un tableau
-jsonPath("$.items", hasSize(3)) // Taille du tableau+jsonPath("$.items.length()"   // Taille du tableau
 </sxh> </sxh>
  
 ==== 3.3 Test avec détection N+1 ==== ==== 3.3 Test avec détection N+1 ====
  
-<sxh java+<sxh kotlin
-import io.hypersistence.utils.jdbc.validator.SQLStatementCountValidator; +import io.hypersistence.utils.jdbc.validator.SQLStatementCountValidator 
-import static io.hypersistence.utils.jdbc.validator.SQLStatementCountValidator.*;+import io.hypersistence.utils.jdbc.validator.SQLStatementCountValidator.*
  
 @Test @Test
 @DisplayName("GET /users/{id}/orders should not trigger N+1 queries") @DisplayName("GET /users/{id}/orders should not trigger N+1 queries")
-void getUserOrders_ShouldNotTriggerNPlusOne() throws Exception {+fun `getUserOrders should not trigger NPlusOne`() {
     // Given     // Given
-    User user = userRepository.save(new User("John", "john@example.com"));+    val user = userRepository.save(User("John", "john@example.com"))
          
-    for (int i = 0; i < 5; i++) { +    repeat(5) { 
-        Order order = new Order(user); +        val order = Order(user = user) 
-        order.addItem(new OrderItem(product1, 1, product1.getPrice())); +        order.addItem(OrderItem(product1, 1, product1.price)) 
-        order.addItem(new OrderItem(product2, 2, product2.getPrice())); +        order.addItem(OrderItem(product2, 2, product2.price)) 
-        orderRepository.save(order);+        orderRepository.save(order)
     }     }
  
     // When     // When
-    SQLStatementCountValidator.reset();+    SQLStatementCountValidator.reset()
          
-    mockMvc.perform(get("/users/{id}/orders", user.getId())+    mockMvc.get("/users/${user.id}/orders"
-        .andExpect(status().isOk()) +        .andExpect 
-        .andExpect(jsonPath("$", hasSize(5))) +            status isOk() } 
-        .andExpect(jsonPath("$[0].items", hasSize(2)));+            jsonPath("$.length()") { value(5) } 
 +            jsonPath("$[0].items.length()") { value(2) 
 +        }
  
     // Then - Vérifier le nombre de requêtes SQL     // Then - Vérifier le nombre de requêtes SQL
-    assertSelectCount(2)// 1 pour User + 1 pour Orders avec items (JOIN FETCH)+    assertSelectCount(2) // 1 pour User + 1 pour Orders avec items (JOIN FETCH)
 } }
 </sxh> </sxh>
Ligne 762: Ligne 837:
  
 <sxh xml> <sxh xml>
-<!-- pom.xml --> 
 <build> <build>
     <plugins>     <plugins>
-        <!-- JaCoCo pour la couverture de code -->+        <!-- JaCoCo Maven Plugin -->
         <plugin>         <plugin>
             <groupId>org.jacoco</groupId>             <groupId>org.jacoco</groupId>
Ligne 771: Ligne 845:
             <version>0.8.11</version>             <version>0.8.11</version>
             <executions>             <executions>
 +                <!-- Préparation de l'agent JaCoCo -->
                 <execution>                 <execution>
 +                    <id>prepare-agent</id>
                     <goals>                     <goals>
                         <goal>prepare-agent</goal>                         <goal>prepare-agent</goal>
                     </goals>                     </goals>
                 </execution>                 </execution>
 +                
 +                <!-- Génération du rapport après les tests -->
                 <execution>                 <execution>
                     <id>report</id>                     <id>report</id>
Ligne 783: Ligne 861:
                     </goals>                     </goals>
                 </execution>                 </execution>
 +                
 +                <!-- Vérification du seuil minimum de couverture -->
                 <execution>                 <execution>
-                    <id>jacoco-check</id>+                    <id>check</id>
                     <goals>                     <goals>
                         <goal>check</goal>                         <goal>check</goal>
Ligne 804: Ligne 884:
                 </execution>                 </execution>
             </executions>             </executions>
-        </plugin> 
- 
-        <!-- Surefire pour les tests unitaires --> 
-        <plugin> 
-            <groupId>org.apache.maven.plugins</groupId> 
-            <artifactId>maven-surefire-plugin</artifactId> 
-            <version>3.2.2</version> 
             <configuration>             <configuration>
-                <includes> 
-                    <include>**/*Test.java</include> 
-                </includes> 
                 <excludes>                 <excludes>
-                    <exclude>**/*IntegrationTest.java</exclude>+                    <!-- Exclure les entités JPA --> 
 +                    <exclude>**/domain/**</exclude> 
 +                    <!-- Exclure les DTOs --> 
 +                    <exclude>**/dto/**</exclude> 
 +                    <!-- Exclure la classe main --> 
 +                    <exclude>**/EcommerceApplicationKt.class</exclude> 
 +                    <!-- Exclure les configurations --> 
 +                    <exclude>**/config/**</exclude>
                 </excludes>                 </excludes>
             </configuration>             </configuration>
-        </plugin> 
- 
-        <!-- Failsafe pour les tests d'intégration --> 
-        <plugin> 
-            <groupId>org.apache.maven.plugins</groupId> 
-            <artifactId>maven-failsafe-plugin</artifactId> 
-            <version>3.2.2</version> 
-            <configuration> 
-                <includes> 
-                    <include>**/*IntegrationTest.java</include> 
-                </includes> 
-            </configuration> 
-            <executions> 
-                <execution> 
-                    <goals> 
-                        <goal>integration-test</goal> 
-                        <goal>verify</goal> 
-                    </goals> 
-                </execution> 
-            </executions> 
         </plugin>         </plugin>
     </plugins>     </plugins>
Ligne 846: Ligne 903:
 ==== 4.2 Commandes Maven ==== ==== 4.2 Commandes Maven ====
  
-<sxh bash;gutter:false+<sxh bash> 
-Tests unitaires uniquement (rapides) +Exécuter les tests et générer le rapport
-mvn clean test +
- +
-# Tests unitaires + rapport de couverture+
 mvn clean test jacoco:report mvn clean test jacoco:report
 +
 +# Vérifier que le seuil de couverture est atteint
 +mvn jacoco:check
  
 # Tous les tests (unitaires + intégration) # Tous les tests (unitaires + intégration)
Ligne 862: Ligne 919:
 ==== 4.3 Exclusion de certaines classes ==== ==== 4.3 Exclusion de certaines classes ====
  
-<sxh xml> +Déjà configuré dans la section précédente. Les exclusions communes pour un projet Kotlin : 
-<configuration> +  * Entités JPA (''domain/*''
-    <excludes> +  * DTOs (''dto/*''
-        <!-- Exclure les entités JPA --> +  Configuration (''config/*'') 
-        <exclude>**/domain/**</exclude> +  * Classe main (''*ApplicationKt.class'')
-        <!-- Exclure les DTOs --> +
-        <exclude>**/dto/**</exclude> +
-        <!-- Exclure la classe main --> +
-        <exclude>**/EcommerceApplication.class</exclude> +
-    </excludes> +
-</configuration> +
-</sxh>+
  
 <WRAP round bloc todo> <WRAP round bloc todo>
Ligne 917: Ligne 967:
  
       - name: Run unit tests       - name: Run unit tests
-        run: mvn clean test+        run: mvn clean test -DskipIntegrationTests
  
       - name: Upload test results       - name: Upload test results
Ligne 926: Ligne 976:
           path: target/surefire-reports/           path: target/surefire-reports/
  
-  # Job 2 : Tests d'intégration (plus longs)+  # Job 2 : Tests d'intégration
   integration-tests:   integration-tests:
     name: Integration Tests     name: Integration Tests
     runs-on: ubuntu-latest     runs-on: ubuntu-latest
-    needs: unit-tests  # Attend que les tests unitaires passent+    needs: unit-tests
          
     steps:     steps:
Ligne 953: Ligne 1003:
           path: target/failsafe-reports/           path: target/failsafe-reports/
  
-  # Job 3 : Analyse de couverture +  # Job 3 : Couverture de code 
-  coverage:+  code-coverage:
     name: Code Coverage     name: Code Coverage
     runs-on: ubuntu-latest     runs-on: ubuntu-latest
-    needs: integration-tests+    needs: [unit-tests, integration-tests]
          
     steps:     steps:
Ligne 970: Ligne 1020:
           cache: maven           cache: maven
  
-      - name: Generate coverage report+      - name: Run tests with coverage
         run: mvn clean verify jacoco:report         run: mvn clean verify jacoco:report
  
-      - name: Upload coverage to Codecov +      - name: Check coverage threshold 
-        usescodecov/codecov-action@v3 +        runmvn jacoco:check
-        with: +
-          files: ./target/site/jacoco/jacoco.xml +
-          flags: unittests +
-          name: codecov-umbrella +
-          fail_ci_if_errorfalse+
  
-      - name: Upload JaCoCo report+      - name: Upload coverage report
         uses: actions/upload-artifact@v3         uses: actions/upload-artifact@v3
         with:         with:
-          name: jacoco-report+          name: coverage-report
           path: target/site/jacoco/           path: target/site/jacoco/
  
-  # Job 4 : Build (optionnel - pour vérifier que l'app compile)+      - name: Comment PR with coverage 
 +        if: github.event_name == 'pull_request' 
 +        uses: codecov/codecov-action@v3 
 +        with: 
 +          files: target/site/jacoco/jacoco.xml 
 +          fail_ci_if_error: true 
 + 
 +  # Job 4 : Build et packaging
   build:   build:
     name: Build Application     name: Build Application
     runs-on: ubuntu-latest     runs-on: ubuntu-latest
-    needs: coverage+    needs: [unit-tests, integration-tests, code-coverage]
          
     steps:     steps:
Ligne 1007: Ligne 1059:
         run: mvn clean package -DskipTests         run: mvn clean package -DskipTests
  
-      - name: Upload artifact+      - name: Upload JAR
         uses: actions/upload-artifact@v3         uses: actions/upload-artifact@v3
         with:         with:
Ligne 1014: Ligne 1066:
 </sxh> </sxh>
  
-==== 5.2 Configuration pour séparer les tests ====+==== 5.2 Séparation des tests (pom.xml) ====
  
 <sxh xml> <sxh xml>
-<!-- pom.xml - Ajout de propriétés --> 
 <properties> <properties>
     <skipUnitTests>false</skipUnitTests>     <skipUnitTests>false</skipUnitTests>
Ligne 1025: Ligne 1076:
 <build> <build>
     <plugins>     <plugins>
 +        <!-- Tests unitaires avec Surefire -->
         <plugin>         <plugin>
             <groupId>org.apache.maven.plugins</groupId>             <groupId>org.apache.maven.plugins</groupId>
Ligne 1030: Ligne 1082:
             <configuration>             <configuration>
                 <skipTests>${skipUnitTests}</skipTests>                 <skipTests>${skipUnitTests}</skipTests>
 +                <includes>
 +                    <include>**/*Test.kt</include>
 +                    <include>**/*Test.class</include>
 +                </includes>
 +                <excludes>
 +                    <exclude>**/*IntegrationTest.kt</exclude>
 +                    <exclude>**/*IntegrationTest.class</exclude>
 +                </excludes>
             </configuration>             </configuration>
         </plugin>         </plugin>
  
 +        <!-- Tests d'intégration avec Failsafe -->
         <plugin>         <plugin>
             <groupId>org.apache.maven.plugins</groupId>             <groupId>org.apache.maven.plugins</groupId>
Ligne 1038: Ligne 1099:
             <configuration>             <configuration>
                 <skipTests>${skipIntegrationTests}</skipTests>                 <skipTests>${skipIntegrationTests}</skipTests>
 +                <includes>
 +                    <include>**/*IntegrationTest.kt</include>
 +                    <include>**/*IntegrationTest.class</include>
 +                </includes>
             </configuration>             </configuration>
 +            <executions>
 +                <execution>
 +                    <goals>
 +                        <goal>integration-test</goal>
 +                        <goal>verify</goal>
 +                    </goals>
 +                </execution>
 +            </executions>
         </plugin>         </plugin>
     </plugins>     </plugins>
Ligne 1046: Ligne 1119:
 ==== 5.3 Badges pour le README ==== ==== 5.3 Badges pour le README ====
  
-<sxh markdown;gutter:false>+<sxh markdown>
 # E-Commerce API # E-Commerce API
  
 ![CI/CD](https://github.com/VOTRE-USERNAME/VOTRE-REPO/actions/workflows/ci.yml/badge.svg) ![CI/CD](https://github.com/VOTRE-USERNAME/VOTRE-REPO/actions/workflows/ci.yml/badge.svg)
 ![Coverage](https://codecov.io/gh/VOTRE-USERNAME/VOTRE-REPO/branch/main/graph/badge.svg) ![Coverage](https://codecov.io/gh/VOTRE-USERNAME/VOTRE-REPO/branch/main/graph/badge.svg)
 +![Kotlin](https://img.shields.io/badge/Kotlin-1.9.22-blue.svg)
 +![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2.0-green.svg)
  
 ## Description ## Description
-API REST pour un système e-commerce avec Spring Boot 3.+API REST pour un système e-commerce avec Spring Boot 3 et Kotlin.
  
 ## Badges de statut ## Badges de statut
Ligne 1075: Ligne 1150:
 mvn test jacoco:report mvn test jacoco:report
 ``` ```
 +
 +## Technologies
 +
 +- Kotlin 1.9.22
 +- Spring Boot 3.2.0
 +- Spring Data JPA
 +- H2 Database
 +- JUnit 5
 +- MockK
 +- JaCoCo
 </sxh> </sxh>
  
Ligne 1106: Ligne 1191:
 ===== Récapitulatif des commandes ===== ===== Récapitulatif des commandes =====
  
-<sxh bash;gutter:false>+<sxh bash>
 # ========== Développement local ========== # ========== Développement local ==========
  
Ligne 1114: Ligne 1199:
 # Lancer l'app en mode test # Lancer l'app en mode test
 mvn spring-boot:run -Dspring-boot.run.profiles=test mvn spring-boot:run -Dspring-boot.run.profiles=test
 +
 +mvn spring-boot:run -P test
  
  
Ligne 1152: Ligne 1239:
 </sxh> </sxh>
  
-===== Livrables attendus (réaliste pour 4h) =====+===== Livrables attendus =====
  
 <WRAP round bloc todo> <WRAP round bloc todo>
-==== Must have (priorité absolue) ====+==== A faire en priorité ====
  
 **Configuration (30min) :** **Configuration (30min) :**
-  * ✅ 4 profils configurés : commun, dev, test, prod +  * 4 profils configurés : commun, dev, test, prod 
-  * ✅ Application démarre avec chaque profil +  * Application démarre avec chaque profil 
-  * ✅ Variables d'environnement documentées+  * Variables d'environnement documentées
  
 **Tests unitaires (1h15) :** **Tests unitaires (1h15) :**
-  * ✅ ''ProductServiceTest'' complet (≥5 tests) +  * ''ProductServiceTest'' complet (≥5 tests) 
-  * ✅ ''UserServiceTest'' complet (≥5 tests) +  * ''UserServiceTest'' complet (≥5 tests) 
-  * ✅ Utilisation correcte des mocks +  * Utilisation correcte des mocks (MockK) 
-  * ✅ Tests paramétrés pour au moins 1 cas +  * Tests paramétrés pour au moins 1 cas 
-  * ✅ Tous les tests passent (''mvn test'')+  * Tous les tests passent (''mvn test'')
  
 **Tests d'intégration (1h15) :** **Tests d'intégration (1h15) :**
-  * ✅ ''ProductControllerIntegrationTest'' complet (≥6 tests) +  * ''ProductControllerIntegrationTest'' complet (≥6 tests) 
-  * ✅ ''UserControllerIntegrationTest'' complet (≥5 tests) +  * ''UserControllerIntegrationTest'' complet (≥5 tests) 
-  * ✅ ''OrderControllerIntegrationTest'' complet (≥4 tests) +  * ''OrderControllerIntegrationTest'' complet (≥4 tests) 
-  * ✅ Vérification des codes HTTP (200, 201, 400, 404, 409) +  * Vérification des codes HTTP (200, 201, 400, 404, 409) 
-  * ✅ Au moins 1 test de détection N+1 +  * Au moins 1 test de détection N+1 
-  * ✅ Tous les tests passent (''mvn verify'')+  * Tous les tests passent (''mvn verify'')
  
 **Couverture + CI/CD (1h) :** **Couverture + CI/CD (1h) :**
-  * ✅ JaCoCo configuré avec seuil minimum 70% +  * JaCoCo configuré avec seuil minimum 70% 
-  * ✅ Workflow GitHub Actions complet (4 jobs) +  * Workflow GitHub Actions complet (4 jobs) 
-  * ✅ Séparation unit tests / integration tests +  * Séparation unit tests / integration tests 
-  * ✅ Badges CI/CD dans le README +  * Badges CI/CD dans le README 
-  * ✅ Pipeline qui passe au vert sur GitHub+  * Pipeline qui passe au vert sur GitHub
  
-==== Nice to have (bonus si temps====+==== En +, Si vous avez le temps ====
  
   * Protection de branche ''main'' configurée   * Protection de branche ''main'' configurée
Ligne 1196: Ligne 1283:
  
 ^ Aspect ^ Test Unitaire ^ Test d'Intégration ^ ^ Aspect ^ Test Unitaire ^ Test d'Intégration ^
-| **Vitesse** | ⚡ Très rapide (<10ms) | 🐌 Plus lent (100-500ms) |+| **Vitesse** | Très rapide (<10ms) | Plus lent (100-500ms) |
 | **Base de données** | ❌ Non (mocks) | ✅ Oui (H2 en mémoire) | | **Base de données** | ❌ Non (mocks) | ✅ Oui (H2 en mémoire) |
 | **Contexte Spring** | ❌ Non | ✅ Oui (toute l'app) | | **Contexte Spring** | ❌ Non | ✅ Oui (toute l'app) |
-| **Annotations** | ''@ExtendWith(MockitoExtension.class)'' | ''@SpringBootTest'' |+| **Annotations** | Pas d'annotation Spring | ''@SpringBootTest'' 
 +| **Mocking** | MockK / Mockito | Vrai composants Spring |
 | **Ce qu'on teste** | Logique métier isolée | Flux complet de bout en bout | | **Ce qu'on teste** | Logique métier isolée | Flux complet de bout en bout |
 | **Quand ça échoue** | Bug dans la logique | Bug d'intégration/config | | **Quand ça échoue** | Bug dans la logique | Bug d'intégration/config |
 | **Commande Maven** | ''mvn test'' | ''mvn verify'' | | **Commande Maven** | ''mvn test'' | ''mvn verify'' |
-| **Fichier de tests** | ''*Test.java'' | ''*IntegrationTest.java'' |+| **Fichier de tests** | ''*Test.kt'' | ''*IntegrationTest.kt'' |
  
 ===== Bonnes pratiques à retenir ===== ===== Bonnes pratiques à retenir =====
Ligne 1209: Ligne 1297:
 <WRAP round bloc info> <WRAP round bloc info>
 ==== Tests unitaires ==== ==== Tests unitaires ====
-  * ✅ **Rapides** : < 10ms par test +  * **Rapides** : < 10ms par test 
-  * ✅ **Isolés** : pas de dépendances externes (DB, réseau) +  * **Isolés** : pas de dépendances externes (DB, réseau) 
-  * ✅ **AAA Pattern** : Arrange, Act, Assert +  * **AAA Pattern** : Arrange, Act, Assert 
-  * ✅ **1 test = 1 comportement** : ne pas tester plusieurs choses +  * **1 test = 1 comportement** : ne pas tester plusieurs choses 
-  * ✅ **Nommage explicite** : ''methodName_WhenCondition_ShouldExpectedBehavior'' +  * **Nommage explicite** : backticks pour noms descriptifs en Kotlin 
-  * ✅ **Mocks minimalistes** : seulement les dépendances nécessaires+  * **Mocks minimalistes** : seulement les dépendances nécessaires 
 +  * **MockK pour Kotlin** : meilleure intégration que Mockito
  
 ==== Tests d'intégration ==== ==== Tests d'intégration ====
-  * ✅ **Réalistes** : données de test cohérentes +  * **Réalistes** : données de test cohérentes 
-  * ✅ **Nettoyage** : ''@Transactional'' ou ''@BeforeEach'' avec ''deleteAll()'' +  * **Nettoyage** : ''@Transactional'' ou ''@BeforeEach'' avec ''deleteAll()'' 
-  * ✅ **Vérifications complètes** : code HTTP + contenu + headers +  * **Vérifications complètes** : code HTTP + contenu + headers 
-  * ✅ **Performance** : détecter les N+1 avec Hypersistence +  * **Performance** : détecter les N+1 avec Hypersistence 
-  * ✅ **Cas d'erreur** : tester les 400, 404, 409, 500+  * **Cas d'erreur** : tester les 400, 404, 409, 500 
 +  * **DSL Kotlin** : utiliser les extensions MockMvc pour Kotlin
  
 ==== CI/CD ==== ==== CI/CD ====
-  * ✅ **Fail fast** : tests unitaires avant intégration +  * **Fail fast** : tests unitaires avant intégration 
-  * ✅ **Parallélisation** : jobs indépendants +  * **Parallélisation** : jobs indépendants 
-  * ✅ **Artifacts** : conserver les rapports et JARs +  * **Artifacts** : conserver les rapports et JARs 
-  * ✅ **Protection** : branche ''main'' protégée +  * **Protection** : branche ''main'' protégée 
-  * ✅ **Documentation** : badges visibles+  * **Documentation** : badges visibles
 </WRAP> </WRAP>
  
Ligne 1235: Ligne 1325:
   * [[https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing|Spring Boot Testing - Documentation officielle]]   * [[https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing|Spring Boot Testing - Documentation officielle]]
   * [[https://junit.org/junit5/docs/current/user-guide/|JUnit 5 User Guide]]   * [[https://junit.org/junit5/docs/current/user-guide/|JUnit 5 User Guide]]
 +  * [[https://mockk.io/|MockK Documentation (Kotlin)]]
   * [[https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html|Mockito Documentation]]   * [[https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html|Mockito Documentation]]
   * [[https://assertj.github.io/doc/|AssertJ Documentation]]   * [[https://assertj.github.io/doc/|AssertJ Documentation]]
   * [[https://www.jacoco.org/jacoco/trunk/doc/maven.html|JaCoCo Maven Plugin]]   * [[https://www.jacoco.org/jacoco/trunk/doc/maven.html|JaCoCo Maven Plugin]]
   * [[https://docs.github.com/en/actions|GitHub Actions - Documentation]]   * [[https://docs.github.com/en/actions|GitHub Actions - Documentation]]
-  * [[https://www.baeldung.com/spring-boot-testing|Baeldung - Testing in Spring Boot]] +  * [[https://www.baeldung.com/kotlin/spring-boot-testing|Baeldung - Testing in Spring Boot with Kotlin]] 
 +  * [[https://kotlinlang.org/docs/jvm-test-using-junit.html|Kotlin Testing with JUnit]]
  • eadl/bloc3/dev_av/td3.1761667650.txt.gz
  • Dernière modification : il y a 9 mois
  • de jcheron