Populando o Banco de Dados com a Anotação @Sql em Testes E2E

Depois de ler esta aula, você entenderá:

  • Por que devemos utilizar a anotação @Sql nos nossos testes End-to-End quando estamos escrevendo testes para uma aplicação Spring Boot.
  • Como podemos popular nosso banco de dados com dados de teste e limpá-lo após cada teste utilizando a anotação @Sql.

Vamos começar descobrindo por que a anotação @Sql é a escolha ideal para gerenciar o estado do banco de dados em testes End-to-End com Spring Boot.


1. Por Que Usar a Anotação @Sql em Testes E2E?

Quando estamos escrevendo testes End-to-End para uma aplicação Spring Boot (iniciada pela própria suíte de testes com @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) ou RANDOM_PORT), devemos utilizar a anotação @Sql para inicializar nosso banco de dados pelos seguintes motivos:

  1. Não podemos usar a anotação @Transactional nos nossos testes End-to-End! Quando nossa aplicação web recebe uma requisição HTTP enviada pelo navegador (Selenium ou Playwright), o servidor HTTP embarcado processa essa requisição em uma thread do servidor separada da thread do JUnit e, portanto, em uma transação de banco de dados completamente diferente.
    • Se o teste inserisse dados dentro de uma transação aberta na thread do teste sem fazer commit, a thread do servidor HTTP não enxergaria esses dados!
    • Além disso, as alterações feitas pela aplicação web durante a requisição HTTP são imediatamente comitadas no banco de dados assim que o controller retorna a resposta HTTP. Um rollback na thread do teste jamais desfaria o que o servidor HTTP comitou.
  2. Não precisamos adicionar bibliotecas extras de teste de banco (como DbUnit) apenas para popular tabelas com SQL puro: a anotação @Sql já faz parte nativa do módulo spring-test.
  3. Podemos inicializar o banco em um estado conhecido antes que cada método de teste seja executado e limpar o banco depois que cada método de teste terminar. Isso elimina completamente o acoplamento entre testes que discutimos na aula anterior!

Vamos ver agora como podemos criar os scripts SQL e configurar a anotação @Sql nas nossas classes de teste End-to-End.


2. Criando os Scripts SQL para os Testes End-to-End

Nossa aplicação de exemplo utiliza o Liquibase para criar o schema do banco de dados quando a aplicação é iniciada. Porém, como queremos ter isolamento total entre cada método de teste End-to-End, utilizaremos três scripts SQL localizados no diretório src/endToEndTest/resources/com/testwithspring/intermediate/:

  1. users.sql: Insere as duas contas de usuário (John Doe e Anne Abbott) na tabela user_accounts.
  2. tasks.sql: Insere as três tarefas de teste na tabela tasks, reinicia a sequence tasks_id_seq em 4 e insere a tag example nas tabelas tags e tasks_tags.
  3. cleandb.sql: Remove todos os dados das tabelas do banco de dados após a execução de cada método de teste, respeitando a ordem das chaves estrangeiras (foreign keys).

Vamos analisar o conteúdo de cada um desses três arquivos:

1. Script users.sql

O script users.sql (src/endToEndTest/resources/com/testwithspring/intermediate/users.sql) insere os dois usuários conhecidos no banco de dados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
INSERT INTO user_accounts
    (id, creation_time, email_address, modification_time, name, password, role, enabled, version)
VALUES
    (
        1,
        '2016-01-13 11:28:39.212',
        'john.doe@gmail.com',
        '2016-01-13 11:28:39.212',
        'John Doe',
        '$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG',
        'ROLE_USER',
        true,
        0
    ),
    (
        2,
        '2016-01-13 11:28:39.212',
        'anne.abbott@gmail.com',
        '2016-01-13 11:28:39.212',
        'Anne Abbott',
        'notUsed',
        'ROLE_USER',
        true,
        0
    );

2. Script tasks.sql

O script tasks.sql (src/endToEndTest/resources/com/testwithspring/intermediate/tasks.sql) insere as três tarefas, atualiza o próximo valor da sequence tasks_id_seq para 4 e associa as tarefas à tag example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
INSERT INTO tasks
    (id, assignee_id, closer_id, creation_time, creator_id, description, modification_time, modifier_id, resolution, status, title, version)
VALUES
    (
        1,
        1,
        null,
        '2016-02-20 12:34:41.588',
        1,
        'This is an example of a task.',
        '2016-02-20 12:34:41.588',
        1,
        null,
        'OPEN',
        'Write example application',
        0
    ),
    (
        2,
        null,
        null,
        '2016-02-20 12:34:41.588',
        2,
        'This is an example of a lesson.',
        '2016-02-20 12:34:41.588',
        2,
        null,
        'OPEN',
        'Write lesson',
        0
    ),
    (
        3,
        2,
        2,
        '2016-02-20 12:34:41.588',
        2,
        'This is an example of a closed task.',
        '2016-02-20 12:34:41.588',
        2,
        'DONE',
        'CLOSED',
        'Delete task',
        0
    );

ALTER SEQUENCE tasks_id_seq RESTART WITH 4;

INSERT INTO tags
    (id, creation_time, modification_time, name, version)
VALUES
    (
        1,
        '2016-02-20 12:34:41.588',
        '2016-02-20 12:34:41.588',
        'example',
        0
    );

INSERT INTO tasks_tags
    (task_id, tag_id)
VALUES
    (1, 1),
    (2, 1),
    (3, 1);

3. Script cleandb.sql

O script cleandb.sql (src/endToEndTest/resources/com/testwithspring/intermediate/cleandb.sql) apaga os registros de todas as tabelas na ordem inversa das dependências de chave estrangeira (tasks_tags -> tags -> tasks -> user_accounts):

1
2
3
4
DELETE FROM tasks_tags;
DELETE FROM tags;
DELETE FROM tasks;
DELETE FROM user_accounts;

3. Configurando a Anotação @Sql na Classe de Teste

Agora podemos utilizar a anotação @Sql (que é uma anotação repetível — @Repeatable(SqlGroup::class)) na nossa classe de teste End-to-End para configurar duas fases distintas de execução:

  1. Antes de cada método de teste (ExecutionPhase.BEFORE_TEST_METHOD — padrão): Executamos os scripts users.sql e tasks.sql para popular o banco de dados em um estado conhecido. Como o script tasks.sql insere linhas na tabela tasks que possuem chaves estrangeiras apontando para user_accounts, devemos garantir que users.sql seja listado antes de tasks.sql.
  2. Depois de cada método de teste (ExecutionPhase.AFTER_TEST_METHOD): Executamos o script cleandb.sql para esvaziar todas as tabelas do banco de dados, garantindo que o próximo teste encontre o banco completamente limpo.

Depois de anotarmos nossa classe de teste End-to-End com @Sql, o código-fonte fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.testwithspring.intermediate.web

import com.testwithspring.intermediate.SeleniumTest
import com.testwithspring.intermediate.TaskTrackerApplication
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.openqa.selenium.WebDriver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.jdbc.Sql
import org.springframework.test.context.jdbc.Sql.ExecutionPhase

@SpringBootTest(
    classes = [TaskTrackerApplication::class],
    webEnvironment = WebEnvironment.DEFINED_PORT
)
@SeleniumTest
@Sql(
    value = [
        "classpath:/com/testwithspring/intermediate/users.sql",
        "classpath:/com/testwithspring/intermediate/tasks.sql"
    ],
    executionPhase = ExecutionPhase.BEFORE_TEST_METHOD
)
@Sql(
    value = ["classpath:/com/testwithspring/intermediate/cleandb.sql"],
    executionPhase = ExecutionPhase.AFTER_TEST_METHOD
)
@ActiveProfiles("endToEndTest")
@Tag("endToEndTest")
class EndToEndTestExample {

    @Autowired
    lateinit var browser: WebDriver

    @BeforeEach
    fun setUp() {
        // Quando este método @BeforeEach é chamado, os scripts users.sql e tasks.sql
        // JÁ FORAM executados e comitados no banco de dados!
    }

    @Test
    fun `deve exibir as tarefas inseridas pelos scripts SQL`() {
        // Ao final deste método @Test, o script cleandb.sql será executado automaticamente.
    }
}

Resumo

Nesta aula, aprendemos que:

  • Não podemos utilizar @Transactional para fazer rollback automático em testes End-to-End porque o servidor HTTP embarcado processa as requisições do navegador em outra thread e comita as transações imediatamente.
  • A anotação @Sql do Spring TestContext Framework nos permite executar scripts SQL comitados antes (BEFORE_TEST_METHOD) e depois (AFTER_TEST_METHOD) de cada método de teste.
  • Ao combinar @Sql(BEFORE_TEST_METHOD) para popular users.sql e tasks.sql com @Sql(AFTER_TEST_METHOD) para executar cleandb.sql, garantimos isolamento completo entre todos os nossos testes End-to-End.
  • No próximo tópico, começaremos a escrever nossos testes End-to-End na prática: localizando elementos HTML, controlando o navegador, criando Page Objects e testando fluxos completos de leitura e submissão de formulários!