
Quando implementamos um programa, procedimento ou classe, temos expectativas sobre seu comportamento:
- Numa função de soma esperamos que ao passar por argumentos os números “3” e “4” o resultado seja “7”.
- Num método de um repositório para inserir um recurso em uma base de dados, que ele seja persistido, o que seria confirmado por uma consulta à base de dados.
Nestas situações, como é que garantimos que nossa solução funciona?
Mutas vezes testando manualmente através do terminal, com print(), interagimos com a interface gráfica, ou com o – misteriosamente esquecido – debugger.
Para um componente simples isso pode parecer suficiente, mas e nos casos em que existem vários fluxos de execução, lançamento de exceções ou complexa mutação de estado?
Teremos que testar todos fluxos possíveis do nosso componente a cada modificação do código-fonte!

A natureza repetitiva dos testes manuais nos leva ao erro e à frustração, concentrando nossos esforços em uma atividade manual em vez de criativa.
Além disso, os testes dão ao desenvolvedor segurança de que a implementação de novas funcionalidades ou a refatoração não produzirão regressões:
"Desenvolvimento orientado a testes é uma forma de administrar o medo durante a programação"- Kent Beck
Desenvolvimento de software é quase sempre uma atividade coletiva. Todos os dias são integradas modificações em repositórios de forma paralela.
É importante garantir que a feature que estamos desenvolvendo não produza efeitos colaterais em outras partes do sistema.
Como é impraticável para o desenvolvedor testar toda a aplicação, uma alternativa é desenvolver uma pipeline de integração continua (CI) com execução de testes automatizados, delegando à máquina a maior parte desse esforço.
Testando o comportamento
Há várias abordagens para implementação de testes automatizados. Aqui vamos nos ater ao proposto por Daniel-Terhorst e Chris Matts no Desenvolvimento Orientado a Comportamento (BDD).
Como próprio nome sugere, a proposta é testar o comportamento dos componentes de forma agnóstica – independente aos detalhes da implementação -.
Nessa proposta um teste consiste por três estágios:
- Given (Dado): a entrada; ou estado inicial da aplicação para testar o comportamento.
- When (Quando): a execucao do comportamento a ser testado
- Then (Então): a comparação entre o comportamento esperado e o comportamento produzido.
Em Python, podemos utilizar o framework Pytest para implementar e executar testes. No contexto de um método de soma:
def test_add():
calc = Calculator() # DADA uma instância de calculadora
result = calc.add(2, 2) # QUANDO eu chamar o método de soma com 2 e 2
assert result == 4 # ENTÃO o resultado deve ser 4
Podemos testar também o lançamento de exceções e mensagens de erro:
def test_divide_by_zero():
calc = Calculator() # DADA uma instância de calculadora
try:
calc.divide(5, 0) # QUANDO eu dividir um número por zero
except ValueError as e:
assert str(e) == 'Cannot divide by zero' # ENTÃO uma exceção deve ser lançada com a mensagem 'Cannot divide by zero'
Com um conjunto abrangente de testes, qualquer pessoa pode rapidamente testar um sistema, estando cobertos os fluxos de falha e sucesso.
Notem que esses são exemplos muito granulares. Estão sendo testados unidades de comportamento isoladamente.
No entanto, as aplicações que desenvolvemos tem um ciclo muito mais complexo. Os dados são processados de forma assíncrona, por vários componentes integrados, trafegam pela rede, são serializados e desserializados…
Essa complexidade tornam relevantes os
Testes de integração (integration tests)
Embora não exista uma definição rigorosa de teste de integração, vamos considerar integração como a circunstancia em que a aplicação interage com sistemas externos, como o sistema de arquivos, um web service ou um banco de dados.

Vamos imaginar um aplicativo de catálogo de filmes usando TypeScript – a variante fortemente tipada do JavaScript – e o framework de testes Jest.
Ela interage com uma base de dados SQL para persistir dados relevantes sobre filmes.
Um filme, entidade Movie, tem os atributos: movie_id, create_at, title, director, rating.
Todas as operações relacionados a interação com o banco de dados são implementadas na camada de repositório com os seguintes métodos:
- save(Movie): insere na tabela movies do banco de dados os atributos de uma entidade Movie.
- getMovieById(movieId): retorna os todos os atributos de um filme por movie_id.
- deleteMovieById(movieId): remove do banco de dados todos os atributos relativos a um movie_id.
export default class MovieRepositoryDatabase implements MovieRepository {
constructor(readonly connection: DatabaseConnection) {}
async save(movie: Movie) {
await this.connection.query(
"insert into movie_store.movies (movie_id, created_at, title, director, rating) values ($1, $2, $3, $4, $5)",
[
movie.getMovieId(),
movie.getCreatedAt().toDateString(),
movie.getMovietitle(),
movie.getDirector(),
movie.getRating(),
],
);
}
async getByMovieById(movieId: string) {
const [movieData] = await this.connection.query(
"SELECT movie_id, created_at, title, director, rating FROM movie_store.movies WHERE movie_id = $1",
[movieId],
);
if (!movieData) return;
return Movie.restore(
movieData.movie_id,
movieData.created_at,
movieData.title,
movieData.director,
movieData.rating,
);
}
async deleteByMovieId(movieId: string) {
await this.connection.query(
"delete from movie_store.movies where movie_id = $1",
[movieId],
);
}
}
Como poderíamos implementar um teste para as operações desse repositório?
Começando pelo método save(movie), seguindo o padrão “Given, When, Then”:
- Given, ou o estado inicial da aplicação para testar o comportamento:
- Banco de dados SQL de nome movie_store e tabela movies, com todos os atributos da entidade Movie: movie_id, create_at, title, director, rating.
- Conexão com o banco de dados, abstraída pela entidade DatabaseConnection.
- Instância de MovieRepository passando a entidade DatabaseConnection por argumento.
- Instância da entidade Movie com os atributos a serem persistidos n banco de dados.
const connection = new DatabaseConnection();
const movieRepository = new MovieRepositoryDatabase(connection);
const movie = new Movie("1", new Date(), "Inception", "Christopher Nolan", 5);
- When, o comportamento a ser testado:
- Invocamos o método save(Movie) do MovieRepository passando a entidade Movie por argumento.
await movieRepository.save(movie);
- Then, a comparação entre o comportamento esperado e o produzido:
- Utilizando o método getByMovieById(movieId) vamos comparar os astributos de Movie persistidos na base de dados com os passados inicialmente.
expect(savedMovie).toEqual(movie);
A implementação completa:
test("deve salvar um filme na base de dados", async () => {
const connection = new DatabaseConnection();
const movieRepository = new MovieRepositoryDatabase(connection);
const movie = new Movie("1", new Date(), "Inception", "Christopher Nolan", 5);
await movieRepository.save(movie);
const savedMovie = await movieRepository.getByMovieById("1");
expect(savedMovie).toEqual(movie);
});
Em comparação com o teste unitário, o teste de integração segue por um fluxo mais próximo do funcionamento normal da aplicação.
Ele garante o funcionamento básico de nosso repositório e a integração da aplicação com o banco de dados.
Mas é um teste mais caro e complexo, porque exige uma uma base de dados execução e integra com outros componentes.
Ainda assim, podemos desenvolver testes ainda mais abrangentes: os
Testes ponta-a-ponta (end-to-end tests)
Em testes ponta-a-ponta, testamos a aplicação a partir da interface do usuário.
Embora nem todas as aplicações tenham interface gráfica (GUI), a predominância de aplicações web levou ao surgimento de frameworks de testes front-end como pupeteer, playwright, cypress.

Com eles é possível interagir com elementos da DOM (document object model) disparar eventos e verificar o seu estado.
Em um exemplo com o framework cypress:
- Given: Acessa o endereço http://localhost:3000
- When:
- Checa existência de elementos de input de um formulário com atributos input[name=”nome” e input[email=”email” na DOM
- Preenche os campos de input
- Simula o clique de submissão de formulário
- Then: Verifica se um elemento contendo “Obrigado, João!” está visível para o usuário.
describe('Exemplo de teste e2e com Cypress', () => {
it('Deve preencher o formulário e verificar o resultado', () => {
cy.visit('http://localhost:3000');
cy.get('input[name="nome"]').type('João Silva');
cy.get('input[email="email"]').type('joao.silva@example.com');email
cy.get('button[type="submit"]').click();
cy.contains('Obrigado, João').should('be.visible');
});
});
Estes são os testes mais caros em termos de infraestrutura necessária, do tempo de execução e das camadas de abstração que abrangem. No entanto, são os que mais se aproximam do fluxo completo de uma aplicação.
A pirâmide de testes
É uma metáfora visual proposta por Mike Cohn para visualizar as diferentes camadas de tipos de teste e o quanto testar em cada uma delas.
Quanto mais alta a integração – como nos testes ponta-a-ponta -, mais cara e lenta é a execução do teste. Na base da pirâmide temos os performáticos testes unitários.

A proposta é concentrar o maior número de testes no nível mais baixo, testando uma ampla variedade de fluxos de execução. E no nível mais alto, testar apenas os principais fluxos de execução.
Conclusão
- Testes automatizados poupam o desenvolvedor de esforços repetitivos e sujeitos a erro.
- Criam um ciclo de feedback rápido para preventivamente identificar bugs; dão segurança para que sejam feitas refatorações e integração continua.
- Existem frameworks para testar inclusive interfaces gráficas de aplicações web.
- Tem diferentes níveis de granularidade e integração, idealmente nos levando a implementar uma maior quantidade de testes de baixo nível.
Recursos úteis
- Livro: Kent Beck – TDD Desenvolvimento Guiado por Testes – 2002
- Curso de Pytest – Harvard: https://library.cfa.harvard.edu/event/testing-python-code-pytest?delta=0
- TDD: Purposes and Practices – https://www.industriallogic.com/blog/tdd-purposes-and-practices/
- A pirâmide de testes: https://martinfowler.com/articles/practical-test-pyramid.html#TheImportanceOftestAutomation
- CI/CD: https://martinfowler.com/articles/continuousIntegration.html
Thiago Guede Willecke1 contribuições
Sou desenvolvedor de software interessado principalmente em backend. Gosto de entender como as coisas funcionam e colaborar para aprender e resolver problemas juntos. Gosto da teoria tanto quanto da prática!