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!
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.
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() })
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) })
É 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!') })
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 .
Escreva alguns testes! (mas comece pequeno)
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!