Este artigo merece sua atenção se você
- São apaixonados por escrever software de boa qualidade e desejam melhorar a estabilidade do seu aplicativo com testes.
- Estão cansados de ver bugs inesperados surgindo em seus sistemas de produção.
- Precisa de ajuda para entender o que são testes automatizados e como abordá-los.
Por que precisamos de testes automatizados?
Como engenheiros, queremos construir coisas que funcionem , mas com cada novo recurso que criamos, inevitavelmente aumentamos o tamanho e a complexidade dos nossos aplicativos.
À medida que o produto cresce, torna-se cada vez mais demorado testar manualmente (por exemplo, com as mãos) todas as funcionalidades afetadas pelas alterações.
A ausência de testes automatizados nos leva a gastar muito tempo e diminuir a velocidade de envio ou a gastar muito pouco para economizar velocidade, resultando em novos bugs no backlog junto com as ligações noturnas do PagerDuty.
Pelo contrário, os computadores podem ser programados para fazer o mesmo repetidamente . Então, vamos delegar os testes aos computadores!
Tipos de testes
A ideia da pirâmide de testes sugere três tipos principais de testes: unidade, integração e ponta a ponta . Vamos nos aprofundar em cada tipo e entender por que precisamos de cada um.
Testes unitários
Uma unidade é um pequeno pedaço de lógica que você testa isoladamente (sem depender de outros componentes).
Os testes unitários são rápidos. Eles terminam em segundos. O isolamento permite que eles sejam executados a qualquer momento, localmente e no CI, sem ativar os serviços dependentes/fazer chamadas de API e de banco de dados.
Exemplo de teste de unidade: uma função que aceita dois números e os soma. Queremos chamá-lo com argumentos diferentes e afirmar que o valor retornado está correto.
// Function "sum" is the unit const sum = (x, y) => x + y test('sums numbers', () => { // Call the function, record the result const result = sum(1, 2); // Assert the result expect(result).toBe(3) }) test('sums numbers', () => { // Call the function, record the result const result = sum(5, 10); // Assert the result expect(result).toBe(15) })
Um exemplo mais interessante é o componente React que renderiza algum texto após a conclusão da solicitação da API. Precisamos simular o módulo API para retornar os valores necessários para nossos testes, renderizar o componente e afirmar que o HTML renderizado tem o conteúdo que precisamos.
// "MyComponent" is the unit const MyComponent = () => { const { isLoading } = apiModule.useSomeApiCall(); return isLoading ? <div>Loading...</div> : <div>Hello world</div> } test('renders loading spinner when loading', () => { // Mocking the API module, so that it returns the value we need jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Loading...').toBeInTheDocument() }) test('renders text content when not loading', () => { // Mocking the API module jest.mock(apiModule).mockReturnValue(() => ({ useSomeApiCall: jest.fn(() => ({ // Return "isLoading: false" for this test case isLoading: false })) })) // Execute the unit (render the component) const result = render(<MyComponent />) // Assert the result result.findByText('Hello world').toBeInTheDocument() })
Testes de Integração
Quando sua unidade interage com outras unidades (dependências) , chamamos isso de integração . Esses testes são mais lentos que os testes de unidade, mas testam como as partes do seu aplicativo se conectam.
Exemplo de teste de integração: um serviço que cria usuários em um banco de dados. Isso requer que uma instância de banco de dados ( dependência ) esteja disponível quando os testes são executados. Testaremos se o serviço pode criar e recuperar um usuário do banco de dados.
import db from 'db' // We will be testing "createUser" and "getUser" const createUser = name => db.createUser(name) // creates a user const getUser = name => db.getUserOrNull(name) // retrieves a user or null test("creates and retrieves users", () => { // Try to get a user that doesn't exist, assert Null is returned const nonExistingUser = getUser("i don't exist") expect(nonExistingUser).toBe(null); // Create a user const userName = "test-user" createUser(userName); // Get the user that was just created, assert it's not Null const user = getUser(userName); expect(user).to.not.be(null) })
Testes ponta a ponta
É um teste ponta a ponta quando testamos o aplicativo totalmente implantado , onde todas as suas dependências estão disponíveis. Esses testes simulam melhor o comportamento real do usuário e permitem detectar todos os problemas possíveis em seu aplicativo, mas são o tipo de teste mais lento .
Sempre que quiser executar testes ponta a ponta, você deverá provisionar toda a infraestrutura e garantir que provedores terceirizados estejam disponíveis em seu ambiente.
Você deseja tê-los apenas para os recursos de missão crítica do seu aplicativo.
Vamos dar uma olhada em um exemplo de teste completo: Fluxo de login. Queremos ir ao aplicativo, preencher os dados de login, enviá-lo e ver a mensagem de boas-vindas.
test('user can log in', () => { // Visit the login page page.goto('https://example.com/login'); // Fill in the login form page.fill('#username', 'john'); page.fill('#password', 'some-password'); // Click the login button page.click('#login-button'); // Assert the welcome message is visible page.assertTextVisible('Welcome, John!') })
Como você escolhe que tipo de teste escrever?
Lembre-se de que os testes ponta a ponta são mais lentos que os testes de integração e os testes de integração são mais lentos que os testes unitários .
Se o recurso no qual você está trabalhando for de missão crítica, considere escrever pelo menos um teste ponta a ponta (como verificar como a funcionalidade Login funciona ao desenvolver o fluxo de autenticação).
Além dos fluxos de missão crítica, queremos testar o máximo possível de casos extremos e vários estados do recurso. Os testes de integração nos permitem testar como as partes do aplicativo funcionam juntas.
Ter testes de integração para endpoints e componentes de clientes é uma boa ideia. Os endpoints devem executar as operações, produzir o resultado esperado e não gerar erros inesperados.
Os componentes do cliente devem exibir o conteúdo correto e responder às interações do usuário de acordo com a forma como você espera que eles respondam.
E finalmente, quando devemos escolher testes unitários ? Todas as pequenas funções que podem ser testadas isoladamente, como sum
que soma os números, Button
que renderiza a tag <button>
, são ótimas candidatas para testes unitários. As unidades são perfeitas se você seguir a abordagem de Desenvolvimento Orientado a Testes .
Qual é o próximo?
Escreva alguns testes! (mas comece pequeno)
- Instale uma estrutura de teste adequada ao seu projeto/linguagem. Cada linguagem possui uma biblioteca popular para testes, como Jest / Vitest para JavaScript, Cypress / Playwright para ponta a ponta (também usa JavaScript), JUnit para Java, etc.
- Encontre uma pequena função em seu projeto e escreva um teste de unidade para ela.
- Escreva um teste de integração para alguma interação componente/serviço-banco de dados.
- Escolha um cenário crítico que possa ser testado rapidamente, como um fluxo de login simples, e escreva um teste completo para isso.
Faça as coisas acima uma vez para entender como funciona. Em seguida, faça isso novamente durante algum trabalho de recurso/bug. Em seguida, compartilhe com seus colegas para que todos façam testes, economizem tempo e durmam melhor à noite!
Recursos úteis:
- Meu blog pessoal
- A Pirâmide do Teste Prático de Ham Vocke
- Desenvolvimento orientado a testes por Martin Fowler