Fermer

avril 7, 2023

H2 Sucks, testez les conteneurs à la rescousse !

H2 Sucks, testez les conteneurs à la rescousse !


Les développeurs sont confus quant à la priorité des cas de test unitaire et des cas de test d’intégration. Bien que les deux soient importants, lorsque nous développons des services pour les clients, nous avons un calendrier serré, donc terminer le développement est en soi un défi, sans parler de la rédaction de cas de test.

Je m’efforce toujours d’écrire des cas de test, quel que soit le temps dont nous disposons. J’insiste sur l’écriture des cas de test d’intégration plutôt que des cas de test unitaire en premier lieu. Dans les cas de tests unitaires, nous cherchons à couvrir un morceau de code critique et à simuler toute sa logique environnante. Mais en production, l’environnement compte autant que ce morceau de code.

Pour écrire des tests d’intégration, nous ne pouvons pas nous moquer des couches du référentiel, et nous avons besoin d’un mécanisme pour résoudre ce problème. La base de données H2 en mémoire a fait le travail pendant des années, bien que très mal. Nous n’avons jamais été en mesure de répliquer la base de données de production pour nos cas de test.

Il est très courant que lors de l’écriture de requêtes SQL, nous utilisions beaucoup de syntaxe MySQL native, que H2 peut ne pas prendre en charge. Les fonctions Windows ou les fonctions analytiques qui sont assez courantes ne sont pas bien supportées par H2. La prise en charge des dernières fonctionnalités sql arrive tardivement dans la base de données H2. Parfois, je dis à mon équipe, essayez d’utiliser les fonctions sql standard sur les fonctions natives mysql et assurez-vous que ces fonctions sont bien prises en charge sur H2 afin que nous couvrions ces extraits de code dans les cas de test. C’est mauvais. Ce n’est pas le cas.

Conteneurs de test à la rescousse.

Nous avons maintenant des conteneurs de test pour presque tous les types de bases de données avec leur dernière version. Alors maintenant, notre base de données de test imite la base de données de production, et je peux tirer pleinement parti de la base de données sous-jacente que j’utilise.

Nous allons maintenant voir à quel point il est facile de configurer des conteneurs de test pour les tests dans Spring Boot.

Examinons notre pom.xml

<dépendance>
<identifiant de groupe>org.springframework.bootidentifiant de groupe>
<ID d’artefact>spring-boot-starter-data-jpaID d’artefact>
dépendance>
<dépendance>
<identifiant de groupe>org.springframework.bootidentifiant de groupe>
<ID d’artefact>spring-boot-starter-webID d’artefact>
dépendance>

<dépendance>
<identifiant de groupe>com.mysqlidentifiant de groupe>
<ID d’artefact>mysql-connecteur-jID d’artefact>
<portée>Duréeportée>
dépendance>
<dépendance>
<identifiant de groupe>org.projectlombokidentifiant de groupe>
<ID d’artefact>LombokID d’artefact>
<facultatif>vraifacultatif>
dépendance>
<dépendance>
<identifiant de groupe>org.springframework.bootidentifiant de groupe>
<ID d’artefact>printemps-boot-starter-testID d’artefact>
<portée>testportée>
dépendance>
<dépendance>
<identifiant de groupe>org.testcontainersidentifiant de groupe>
<ID d’artefact>junit-jupiterID d’artefact>
<portée>testportée>
dépendance>
<dépendance>
<identifiant de groupe>org.testcontainersidentifiant de groupe>
<ID d’artefact>mysqlID d’artefact>
<portée>testportée>
dépendance>

<dépendance>
<identifiant de groupe>org.springframework.bootidentifiant de groupe>
<ID d’artefact>spring-boot-starter-webfluxID d’artefact>
<version>3.0.2version>
dépendance>

dépendances>
<DépendanceGestion>
<dépendances>
<dépendance>
<identifiant de groupe>org.testcontainersidentifiant de groupe>
<ID d’artefact>testcontainers-bomID d’artefact>
<version>${testcontainers.version}version>
<taper>pompontaper>
<portée>importerportée>
dépendance>
dépendances>
DépendanceGestion>

La dépendance principale a été marquée dans les commentaires.

Considérez maintenant le Classe OrderEntity.java, OrderEntityRepository.java et OrderController.java

package org.anil.testcontainers.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name ="order_entity")
public class OrderEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String orderName;
    private Long orderType;
    @Transient
    private String orderTypeDes;
}
package org.anil.testcontainers.repository;

import org.anil.testcontainers.entity.OrderEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderEntityRepository  extends JpaRepository<OrderEntity,Long> {
}
@RestController
public class OrderController {
    @PersistenceContext
    EntityManager entityManager;

    @Autowired
    OrderEntityRepository orderEntityRepository;

    @PostMapping("/save")
    public ResponseEntity<OrderEntity> saveOrderEntity(@RequestBody OrderEntity orderEntityDto){
       OrderEntity orderEntity = new OrderEntity();
       orderEntity.setOrderName(orderEntityDto.getOrderName());
       orderEntity.setOrderType(orderEntityDto.getOrderType());
        orderEntityRepository.save(orderEntity);
       return ResponseEntity.ok(orderEntity);
    }

    @GetMapping("/allorders")
    public ResponseEntity<List<OrderEntity>> saveOrderEntity(){
        var query  =entityManager.createNativeQuery("""
                    select id, 
                    order_name,
                    order_type,
                    if(order_type="1",'FullFilled','Cancelled') from order_entity ; 
            """);
        
        
       var resultList = (List<Object[]>) query.getResultList();
       List<OrderEntity> list = new ArrayList<>();
       for(Object[] object : resultList){
           var id = Long.valueOf(object[0].toString());
           var orderName = object[1].toString();
           var orderType = Long.valueOf(object[2].toString());
           var orderTypeDesc = object[3].toString();
           OrderEntity orderEntity = new OrderEntity();
           orderEntity.setId(id);
           orderEntity.setOrderTypeDes(orderTypeDesc);
           orderEntity.setOrderType(orderType);
           list.add(orderEntity);
       }
       return ResponseEntity.ok(list);
    }

Nous écrirons des cas de test d’intégration pour les méthodes du contrôleur ci-dessus.

Dans OrderControllerTest.java, nous allons d’abord configurer notre conteneur de test MySQL.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@Testcontainers
class OrderControllerTest {

    static final MySQLContainer MY_SQL_CONTAINER;

    @Autowired
    WebTestClient webTestClient;

    @Autowired
    private OrderEntityRepository  orderEntityRepository;

    static {
        MY_SQL_CONTAINER = new MySQLContainer("mysql:latest");
        MY_SQL_CONTAINER.start();
    }

Nous devons d’abord annoter notre classe Test avec @Testcontainers, puis dans le bloc statique, nous devons créer un nouveau MySQLContainer avec la dernière image MySQL Docker. Pour la première fois, il extraira l’image MySQL avec la balise « latest ». Docker doit être installé sur votre système pour fonctionner avec des conteneurs.

Et ensuite, nous démarrons le conteneur MySQL avec une méthode start(). Une fois les cas de test terminés, ce conteneur MySQL sera arrêté et supprimé de la liste de Docker. Tout comme en mémoire H2, le conteneur sera monté et démonté à la sortie du test.

Nous devons informer Spring Boot des propriétés de cette base de données pour le conteneur de test, et Spring a une bonne façon de le faire.

@DynamicPropertySource
    static void configureTestProperties(DynamicPropertyRegistry registry){
        registry.add("spring.datasource.url",() -> MY_SQL_CONTAINER.getJdbcUrl());
        registry.add("spring.datasource.username",() -> MY_SQL_CONTAINER.getUsername());
        registry.add("spring.datasource.password",() -> MY_SQL_CONTAINER.getPassword());
        registry.add("spring.jpa.hibernate.ddl-auto",() -> "create");

    }

Configurons BeforeEach et AfterEach pour ce cas de test.

 @BeforeEach
    public void beforeEach(){
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderType(1L);
        orderEntity.setOrderName("order-1");
        orderEntityRepository.save(orderEntity);
    }
    @AfterEach
    public void afterEach(){
        orderEntityRepository.deleteAll();
    }

Écrivons le cas de test pour la méthode save

@Test
    void saveOrderEntity() {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setOrderType(1L);
        orderEntity.setOrderName("order-2");
        webTestClient.post()
                .uri("/save")
                .bodyValue(orderEntity)
                .exchange()
                .expectHeader()
                .contentType(MediaType.APPLICATION_JSON)
                .expectStatus()
                .is2xxSuccessful()
                .expectBody(OrderEntity.class)
                .consumeWith(orderentity -> Assertions.assertNotNull(orderentity.getResponseBody().getId()));
    }

Et maintenant pour obtenir la méthode

  @Test
    void getOrderEntity() {
        webTestClient.get()
                .uri("/allorders")
                .exchange()
                .expectHeader()
                .contentType(MediaType.APPLICATION_JSON)
                .expectStatus()
                .is2xxSuccessful()
                .expectBodyList(OrderEntity.class)
                .consumeWith(listOfObject ->{
                   var list  = listOfObject.getResponseBody();
                    Assertions.assertTrue(list.size() == 1);
                    Assertions.assertTrue(list.get(0).getOrderTypeDes().equals("FullFilled"));
                });
    }

Lorsque nous exécutons les cas de test, l’image MySQL sera extraite lors de la première exécution et de nombreux journaux seront générés. Les conteneurs de test ont fourni logback.xml recommandé pour se débarrasser de la verbosité.

Voici le logback-test.xml qui peut être inclus dans le dossier de ressources.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="org.testcontainers" level="INFO"/>
    <logger name="com.github.dockerjava" level="WARN"/>
    <logger name="com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire" level="OFF"/>
</configuration>

Nous pouvons voir que le conteneur de test MySQL a démarré. Nous pouvons également voir l’icône de la baleine Docker.

J’aime beaucoup les conteneurs de test, car j’écris d’excellents cas de test qui imitent mon environnement de production. Je suis assez confiant lorsque mon code entre en production.

Veuillez trouver le code sur mon lien GitHub

https://github.com/anilgola90/testcontainers

Le lien du blog original est ici

Voir sur Medium.com




Source link