Resetando Sequences e IDs no PostgreSQL
Ao concluir esta lição, você será capaz de:
- Compreender por que precisamos resetar os valores atribuídos às colunas
id(sequences e colunas identity) das tabelas do nosso banco de dados. - Saber como criar um componente reutilizável (
IdColumnReset) que reinicia os valores atribuídos a colunas de chave primária auto-incrementadas. - Resetar os valores dessas colunas de forma determinística no método de setup (
@BeforeEach) antes da execução de cada método de teste.
Esta lição assume que:
1. Por Que se Preocupar com Isso?
Um dos problemas fundamentais em testes automatizados é que não podemos fazer asserções sobre o desconhecido. Com muita frequência nos deparamos com a seguinte situação:
Queremos escrever múltiplos testes de integração para um método que insere novas informações no banco de dados, pois seguimos a boa prática de escrever testes granulares onde cada método valida um único conceito lógico.
Se tentarmos escrever asserções para os valores de colunas id que são gerados automaticamente pelo banco de dados (via IDENTITY, SERIAL ou SEQUENCE), notaremos que nossos testes falham de maneira intermitente ou dependente da ordem de execução, pois o JUnit não garante a ordem em que os métodos de teste são invocados — e a cada inserção o contador interno do banco avança (1, 2, 3…).
Nota:
Embora exista a anotação
@TestMethodOrderno JUnit Jupiter para forçar a ordem de execução dos métodos de teste, não devemos utilizá-la para contornar esse problema, pois testes bem projetados devem ser completamente independentes e jamais depender do efeito colateral de outros testes.Além disso, vale entender que fazer asserções na coluna
idpode não ser estritamente necessário se estivermos testando uma funcionalidade que insere dados em uma única tabela isolada. Contudo, se a funcionalidade insere dados em múltiplas tabelas relacionadas, é indispensável verificar que as “linhas filhas” (ex:tasks_tags) foram vinculadas à “linha pai” correta (task_id = 1).
Se estivéssemos usando apenas um banco de dados em memória (como H2), poderíamos “resolver” esse problema seguindo estes dois passos:
- Anotar nossa classe de teste com
@DirtiesContext. - Configurar o atributo
classModeda anotação@DirtiesContextparaDirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD.
Leitura Adicional:
Isso resolveria o sintoma em bancos em memória porque a aplicação recria o banco em memória toda vez que o ApplicationContext é carregado, e @DirtiesContext força a destruição e recriação do contexto do Spring após cada método de teste. Essa técnica só é aceitável se a aplicação tiver meia dúzia de testes de integração rodando em memória.
Dito isso, na imensa maioria das vezes não devemos usar @DirtiesContext porque:
- Ela causa uma degradação catastrófica na performance da suíte de testes de integração (recarregar o Hibernate e o Spring Boot a cada teste custa vários segundos).
- Ela não funciona quando executamos nossos testes de integração contra um banco de dados real (como PostgreSQL via Testcontainers), pois recriar o
ApplicationContextnão apaga nem reinicia as sequences do container PostgreSQL em execução.
Em vez disso, devemos criar uma forma rápida e direta de resetar os valores gerados automaticamente antes da execução de cada método de teste. Vamos construir um componente que faz exatamente isso.
2. Criando o Nosso Componente (IdColumnReset)
Vamos criar um componente que cumpra os seguintes requisitos:
- O próximo valor atribuído a uma coluna de chave primária auto-incrementada (ou sequence) deve ser
1. - O nome da coluna de chave primária padrão é
id. - Os nomes das tabelas cujas colunas
idserão resetadas são informados pelo chamador do componente.
Nota: Embora esta lição enfatize colunas auto-incrementadas (
IDENTITY), exatamente a mesma técnica se aplica ao reset deSEQUENCEs no PostgreSQL (ALTER SEQUENCE <table_name>_id_seq RESTART WITH 1ouALTER TABLE <table_name> ALTER COLUMN id RESTART WITH 1para colunasGENERATED BY DEFAULT AS IDENTITY).
Podemos construir esse componente passo a passo:
Primeiro, criamos uma classe chamada IdColumnReset:
|
|
Segundo, declaramos um template de instrução SQL que garante que o próximo valor atribuído à coluna id da tabela especificada seja 1.
Após adicionarmos a constante QUERY_TEMPLATE, o código-fonte da classe IdColumnReset ficará assim:
|
|
Suporte a Múltiplos Bancos de Dados:
Se você precisar suportar múltiplos bancos de dados com sintaxes SQL distintas na mesma suíte, pode extrair o template SQL para o arquivo
application-integrationTest.ymle injetá-lo no construtor doIdColumnResetvia@Value("\${test.reset.sql.template}").
Terceiro, adicionamos uma propriedade NamedParameterJdbcTemplate na classe IdColumnReset e implementamos o construtor para injeção de dependência:
|
|
Quarto, implementamos o método que reseta os valores atribuídos às colunas id das tabelas informadas. Esse método se chama resetIdColumns() e recebe os nomes das tabelas como vararg tableNames: String:
|
|
Agora temos um componente capaz de reiniciar o contador id das tabelas especificadas em menos de 1 milissegundo. No entanto, antes de podermos injetá-lo nos nossos testes de integração, precisamos registrá-lo na configuração de contexto dos testes.
3. Configurando o Application Context dos Testes de Integração
Antes de usar o nosso novo componente, precisamos declarar um bean IdColumnReset na classe de configuração dos nossos testes de integração (IntegrationTestContext / @TestConfiguration).
Após adicionarmos esse bean, o código-fonte da nossa classe de configuração ficará assim:
|
|
Nota: No Spring Boot 4.1, o starter
spring-boot-starter-jdbcouspring-boot-starter-data-jpajá auto-configura automaticamente o beanNamedParameterJdbcTemplate. Caso sua aplicação não use auto-configuração JDBC do Spring Boot, basta declarar o@Bean fun namedParameterJdbcTemplate(dataSource: DataSource) = NamedParameterJdbcTemplate(dataSource)na própria classeIntegrationTestContext.
Com o contexto configurado, podemos finalmente utilizar o IdColumnReset nos testes de integração que inserem novos registros no banco.
4. Resetando os Valores Gerados da Coluna ID nos Testes
Podemos resetar o valor gerado para a coluna id da tabela tasks seguindo estes dois passos:
Primeiro, injetamos o bean IdColumnReset na nossa classe de teste de integração via @Autowired.
Após adicionarmos esse campo, nossa classe de teste ficará assim:
|
|
Segundo, precisamos resetar o valor gerado para a coluna id da tabela tasks antes da execução de cada método de teste. Fazemos isso invocando idColumnReset.resetIdColumns("tasks") dentro do método anotado com @BeforeEach.
Após adicionarmos a chamada de reset no método de setup, o código completo da configuração da nossa classe de teste ficará assim:
|
|
Agora podemos escrever asserções determinísticas para o id da tarefa recém-criada (por exemplo, verificando o redirecionamento para /task/1 ou validando <tasks id="1" .../> no @ExpectedDatabase), pois ele será sempre 1, independentemente da ordem em que os testes forem executados!
Vamos resumir o que aprendemos nesta lição.
Resumo
Esta lição nos ensinou quatro pontos fundamentais:
- Não devemos usar a anotação
@DirtiesContextse nossa aplicação tiver muitos testes de integração e/ou se executarmos nossos testes contra um banco de dados real (como PostgreSQL via Testcontainers). - Se precisarmos suportar múltiplos bancos de dados com dialetos diferentes, devemos externalizar o template da instrução SQL para um arquivo de propriedades.
- Podemos usar exatamente a mesma técnica do
IdColumnResettanto para colunas auto-incrementadas (IDENTITY) quanto paraSEQUENCEs de banco de dados. - Devemos sempre invocar o reset das colunas de ID geradas dentro do método de setup (
@BeforeEach) antes de cada método de teste.