Search code examples
javamysqlspring-bootjunitjunit5

How can I delete all database entities after the tests are completed?


I was looking if there is a way with some kind of annotation rather than with code to clean the database after a test generated registries in it. I found about @Dirtiescontext but it doesn't seem to be working. I don't know if I'm not using it right or what the problem is. I am using Spring Boot 3 and MySQL 8.

This is my test code:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
@DisplayName("Articles Integration Tests")
public class ArticlesControllerIT {
    @Autowired
    private TestRestTemplate restTemplate;
    @Autowired
    private ArticlesService articlesService;
    @Autowired
    private ModelMapper modelMapper;
    @Autowired
    private ArticleRepository articleRepository;

    @Test
    @Sql("/create_suppliers.sql")
    @DisplayName("Happy path: Create articles")
    void WHEN_calling_create_articles_THEN_status_is_200() {
        final List<ArticleDTO> articleDTOS = Instancio.ofList(ArticleDTO.class).size(3)
                .ignore(field(ArticleDTO::getId))
                .ignore(field(ArticleDTO::getCategory))
                .supply(all(SupplierDTO.class), random -> SupplierDTO.builder()
                        .id(random.intRange(1, 10))
                        .build())
                .create();
        final CreateArticlesRequest createArticlesRequest = CreateArticlesRequest.builder()
                .articles(articleDTOS)
                .user(Instancio.create(String.class))
                .build();
        final ResponseEntity<CreateArticlesResponse> response = restTemplate.exchange(restTemplate.getRootUri() + URIConstants.ARTICLES_URI,
                HttpMethod.POST, new HttpEntity<>(createArticlesRequest), new ParameterizedTypeReference<>() {});
        final List<ArticleDTO> articles = Objects.requireNonNull(response.getBody()).getArticles();

        //Remove ID from the response articicles to check that content is the same as the articles to be created
        articles.forEach(article -> article.setId(null));

        assertEquals(articles.size(), articleDTOS.size() , "articles.size() should be " + articleDTOS.size());
        assertTrue(articles.containsAll(articleDTOS), "articles should contain the same elements as articulosDTOS");
        assertEquals(HttpStatus.OK, response.getStatusCode(), "Status Code should be " + HttpStatus.OK);
    }
}

My service method that creates the registries:

@Service
public class ArticlesServiceImpl implements ArticlesService {
    private final ArticleRepository articleRepository;
    private final ModelMapper modelMapper;
    private final SuppliersService suppliersService;

    @Autowired
    ArticlesServiceImpl(final ArticleRepository articleRepository, final ModelMapper modelMapper, final SuppliersService suppliersService) {
        this.articleRepository = articleRepository;
        this.modelMapper = modelMapper;
        this.suppliersService = suppliersService;
    }

    @Override
    public List<Article> createArticles(final List<Article> articles, final String usuario) {
        final List<ArticleDAO> articleDAOS;

        this.suppliersService.checkSuppliersExist(articles
                .stream()
                .map(Article::getSupplier)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        this.checkArticlesDontExistByName(articles);
        articles.forEach(article -> article.setUser(usuario));
        articleDAOS = this.modelMapper.map(articles, new TypeToken<List<ArticleDAO>>(){}.getType());
        this.articleRepository.saveAll(articleDAOS);
        return this.modelMapper.map(articleDAOS, new TypeToken<List<Article>>(){}.getType());
    }
}

My repository:

@Repository
public interface ArticleRepository extends JpaRepository<ArticleDAO, Integer> {
    boolean existsByName(final String name);
}

My entity:

@Getter
@Setter
@NoArgsConstructor
@Entity
@EqualsAndHashCode
@Table(name = "article")
public class ArticleDAO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "selling_price")
    private BigDecimal sellingPrice;

    @Column(name = "supplier_price")
    private BigDecimal supplierPrice;

    @Column(name = "user")
    private String user;

    @Column(name = "selling_unit_type")
    private String sellingUnitType;

    @Column(name = "inventory_amount")
    private Integer inventoryAmount;

    @Column(name = "picture")
    private String picture;

    @Column(name = "color")
    private String color;

    @Column(name = "reference")
    private String reference;

    @Column(name = "bar_code")
    private String barCode;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "category_id", referencedColumnName = "id")
    private CategoryDAO category;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "supplier_id", referencedColumnName = "id")
    private SupplierDAO supplier;
}

Am I using @DirtiesContext wrong, or this is just not the way?


Solution

  • Whether the @DirtiesContext annotation will clean persisted data in the test database depends on how you create this database in the test.

    If you are using embedded H2 in-memory database, then @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) will cause Spring to discard the ApplicationContext and create a new one after each test method, including the destruction and re-creation of the H2 in-memory database, so it effectively "cleans" the database state between tests. But if you create your test database, for example, using Testcontainers, then @DirtiesContext will not necessarily affect the test container with the database.

    I would recommend using SQL scripts to clean up the database after each test with the @Sql annotation, just like you do to create test data:

    @Sql(scripts = "/truncateTables.sql", executionPhase = AFTER_TEST_METHOD)
    

    From my point of view, this is the most reliable method.

    There is also an option to use @Transactional annotation to rollback test transaction after each test, but this option will not work for you due to loading the real web environment using SpringBootTest.WebEnvironment.RANDOM_PORT. More info about it in the documentation:

    If your test is @Transactional, it rolls back the transaction at the end of each test method by default. However, as using this arrangement with either RANDOM_PORT or DEFINED_PORT implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. Any transaction initiated on the server does not roll back in this case.