TestContainers (Roberto Bolgheroni e Lucas Quaresma)
Testes automatizados são essenciais para o desenvolvimento de software. Eles permitem a detecção precoce de erros e previnem regressões após cada alteração no código. Com a automação, é possível realizar verificações repetitivas e abrangentes sem a necessidade de intervenção manual, o que aumenta a eficiência e a consistência do processo de desenvolvimento. Dessa forma, os desenvolvedores podem implementar mudanças com mais segurança, sabendo que o sistema continua operando de maneira confiável.
Testes de integração são particularmente importantes para verificar a comunicação do sistema com dependências externas, como bancos de dados, serviços de terceiros e APIs. Enquanto os testes unitários focam em componentes isolados, os testes de integração asseguram que diferentes partes do sistema interajam corretamente entre si e com elementos externos. Essa abordagem ajuda a identificar e corrigir problemas de integração que podem não ser evidentes quando os componentes são testados isoladamente, garantindo uma operação harmoniosa e eficiente do sistema como um todo.
Para lidar com dependências externas nos testes de integração, duas abordagens principais podem ser utilizadas: mocks e dependências concretas. Mocks são simulações de componentes externos que permitem testar o comportamento do sistema em um ambiente controlado e previsível. Eles são úteis para isolar o sistema de variáveis externas e focar no comportamento interno. Por outro lado, utilizar dependências concretas envolve testar com instâncias reais das dependências do sistema, proporcionando uma validação mais realista e abrangente. Veja mais sobre essa distinção a seguir.
Mocks e Testes de Integração
[editar | editar código-fonte]Mocks substituem o código de produção, diminuindo a proteção que os testes automatizados proporcionam sobre o funcionamento real do sistema. Quanto mais próximos os testes são do ambiente de produção, melhor é a cobertura que proporcionam. O uso excessivo de mocks pode gerar falsa confiança e falsos positivos. Um exemplo clássico é quando uma mudança na modelagem do banco de dados torna uma operação inválida. Caso o componente de código que interage com o banco (geralmente uma classe "Repositório") seja mockado, essa falha pode passar despercebida durante os testes.
Além disso, mocks tornam a bateria de testes mais "frágil" a refatorações. Sempre que a comunicação entre a dependência mockada e o componente sob teste mudar, pode ser necessário rever os mocks, pois eles podem gerar falsos negativos. Claro, nem sempre é possível substituir um mock por uma instância concreta. No entanto, principalmente quando a dependência é interna do projeto, como um componente de acesso a banco de dados interno, é boa prática utilizar uma implementação concreta nos testes de integração.
Utilizando Dependências Concretas
[editar | editar código-fonte]Manter uma instância concreta de uma dependência para uso nos testes automatizados pode ser desafiador. Um banco de dados de testes precisa ser disponibilizado, preenchido com dados e deve refletir as mudanças na modelagem aplicadas no banco de produção. Uma alternativa comum é a utilização de bancos em memória, como H2. No entanto, utilizar um modelo de banco de dados para testes diferente do banco de produção pode gerar falsos negativos – funcionalidades específicas do H2 podem não existir ou não funcionar da mesma forma no Postgres, por exemplo.
Para diminuir essa complexidade, podemos utilizar a biblioteca TestContainers.
O que é TestContainers?
[editar | editar código-fonte]TestContainers é uma biblioteca Java/Kotlin que facilita a execução de testes de integração usando contêineres Docker. Ele fornece uma maneira conveniente de iniciar contêineres Docker temporários durante a execução dos testes, permitindo que você teste suas aplicações em um ambiente controlado e isolado. O projeto disponibiliza instâncias leves, descartáveis e eficazes de bancos de dados, Selenium, Message Brokers ou quaisquer outras dependências que possam ser executadas em um contêiner Docker. Ele cuida de todas as fases dos contêineres Docker e se conecta com o JUnit, tornando o processo ainda mais simples. Seu foco principal é garantir que seus testes de integração se assemelhem o máximo possível ao ambiente de produção.
A seguir, encontra-se um tutorial para a aplicação de TestContainers em um projeto Kotlin.
Pré-requisitos
[editar | editar código-fonte]Antes de começar, certifique-se de ter o Kotlin e um sistema de construção como o Maven/Gradle configurados em seu ambiente de desenvolvimento. Para esse tutorial, seguiremos com o Maven e Spring.
Passo 1: Configurando o Projeto
[editar | editar código-fonte]
Estamos utilizando um projeto já existente, configurado com Maven e Spring, então apenas adicionamos as seguintes dependências no arquivo pom.xml
,
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.18.0</version>
<scope>test</scope>
</dependency>
Passo 2: Escrevendo o Teste
[editar | editar código-fonte]Agora, vamos criar um teste de integração Kotlin para verificar a interação com o banco de dados MySQL usando o Testcontainers.
Primeiro, construímos uma classe DatabaseContainer
para poder ter instancias da classe de testes, e poder aproveitar essa.
class DatabaseContainer private constructor() : MySQLContainer<DatabaseContainer>("mysql:8.1.0") {
override fun start() {
super.start()
System.setProperty("spring.datasource.url", mySQLContainer!!.getJdbcUrl())
System.setProperty(
"spring.datasource.username",
mySQLContainer!!.getUsername()
)
System.setProperty(
"spring.datasource.password",
mySQLContainer!!.getPassword()
)
}
override fun stop() {
// DO NOTHING
}
companion object {
private var mySQLContainer: DatabaseContainer? = null
val instance: DatabaseContainer?
get() {
if (mySQLContainer == null) {
mySQLContainer = DatabaseContainer()
.withDatabaseName("test")
?.withUsername("test")
?.withPassword("test")
}
return mySQLContainer
}
}
}
Agora, para testar o comportamento dessa classe:
@Service
class TaskService (private val taskRepository: TaskRepository, private val taskListRepository: TaskListRepository) {
fun updateData(taskId: Long, updateTaskDto: UpdateTaskDto): Task {
var queryResult = taskRepository.findById(taskId)
if(queryResult.isEmpty) {
throw IllegalArgumentException("Task Not Found");
}
var taskFound = queryResult.get()
taskFound.title = updateTaskDto.title
taskFound.description = updateTaskDto.description
return taskRepository.save(taskFound)
}
// ...
}
Iremos construir os testes de integração, para verificar a funcionalidade do método updateData()
:
@SpringBootTest
class TaskServiceIntegrationTests @Autowired constructor(private val taskService: TaskService, private val taskRepository: TaskRepository){
@BeforeEach
@Transactional
fun cleanUp() {
taskRepository.deleteAll()
}
@Test
fun `should throw exception when task doesn't exist`() {
try {
taskService.updateData(1, UpdateTaskDto("", ""))
fail("Expected an IllegalArgumentException to be thrown")
} catch (e: Exception) {
assertTrue(e is IllegalArgumentException)
}
}
@Test
fun `should update data when task exists`() {
// arrange
var task: Task = taskRepository.save(Task("oldTitle", "oldDescription", 1))
// act
task = taskService.updateData(1, UpdateTaskDto("newTitle", "newDescription"))
assertEquals(task.title, "newTitle")
assertEquals(task.description, "newDescription")
}
companion object {
@JvmField
@ClassRule
var mySQLContainer: DatabaseContainer? = DatabaseContainer.instance
@JvmStatic
@BeforeAll
fun setUp(): Unit {
mySQLContainer!!.start()
}
}
}
Conclusão
[editar | editar código-fonte]O uso do TestContainers em testes de integração proporciona uma maneira eficiente e confiável de testar dependências externas em um ambiente controlado, semelhante ao de produção. A biblioteca facilita a criação e o gerenciamento de contêineres Docker temporários, garantindo testes mais realistas e precisos. Implementar TestContainers com Kotlin, Maven e Spring melhora a qualidade do software, reduzindo erros em produção e aumentando a confiança nas interações do sistema. Adotar essa prática é um passo essencial para uma infraestrutura de testes robusta e eficaz.
Referências
[editar | editar código-fonte]https://java.testcontainers.org/
https://www.baeldung.com/docker-test-containers
https://kotest.io/docs/extensions/test_containers.html
https://medium.com/@dpeachesdev/using-testcontainers-with-kotlin-springboot-be248f33a3cc