Teste Estático, Unitário, de Integração e End-to-End Explicado - Testing Series #1

Uma explicaçãp das quatro principais categorias de testes de software

Há centenas de diferentes categorias de testes de software, como testes de desempenho, testes funcionais, testes visuais, testes de usabilidade, e muitos outros. Mas de todas estas categorias, há quatro que continuam aparecendo na maioria dos projetos. Eles são:

  1. Testes estáticos
  2. Testes Unitários
  3. Testes de Integração
  4. Testes End-to-end

Neste artigo, analisaremos cada uma destas quatro categorias mais populares, para que você possa entender suas diferenças e decidir quais devem ser usadas em seu projeto.

LinkO que é teste de software

Antes de mais nada: O que é teste de software?

O teste de software é o processo de avaliação do software para garantir que ele evite regressões e não introduza novos bugs. Em outras palavras, é apenas para garantir que o software faça o que deve fazer.

Há muitas maneiras de se fazer isso. Se você quiser conhecer todas elas, você pode verificar as referências no final. Neste artigo, vamos explorar as quatro opções mais populares:

  1. Testes estáticos
  2. Testes Unitários
  3. Testes de Integração
  4. Testes End-to-end

LinkTestes Estáticos

Um teste estático significa que estamos testando o código sem executá-lo. É por isso que é chamado de estático.

Podemos usar isto para saber da existência de erros de digitação, erros de tipagem, e muitas outros problemas. Aqui estão alguns exemplos de testes estáticos:

LinkLinting

Linting é o processo de verificação de seu código fonte para aplicar as convenções estilísticas e medidas de segurança.

Isto é feito usando uma ferramenta de lint, também conhecida como linter. Os linters estão disponíveis para a maioria das linguagens. Alguns linters de renome são ESLint, CSSLint, e Pylint.

Esse é um exemplo de como uma regra da ESlint funcionaria.

JavaScript
var someFunction = () => {
  //=> 🚨 ESLINT (no-var): Unexpected var, use let or const instead
  console.log('someFunction');
};

Neste exemplo, estamos usando a regra ESLint no-var para impor o uso de declarações let e const em vez de declarações var. Então o ESLint está estaticamente analisando nosso código sem executá-lo para garantir que não estamos usando var. Mas já que estamos estamos usando var, o ESLint está nos avisando.

Este é apenas um exemplo simples de um linting check, mas os linters são muito mais poderosos do que isso. Eles podem verificar mil regras diferentes, e você também pode escrever suas próprias regras personalizadas, se necessário.

Você pode até mesmo ter a auto-fixação de algumas regras de linting, tais como a substituição automática de declarações var por declarações let ou const.

Outra coisa incrível que você pode fazer é criar uma configuração base de linting, e então você pode reutilizar essa configuração em todos os seus projetos. Muitas empresas fazem isso. Facebook, Airbnb, e outras empresas criaram suas próprias convenções de estilo e configurações de linting que fazem cumprir essas convenções de estilo. Em seguida, eles reutilizam essas regras de linting para seus projetos.

LinkChecagem da tipagem

Em linguagens de programação, temos sistemas de tipagem fortes e fracos. Quando o sistema de tipagem é forte, o compilador nos avisa no caso de erros de digitação e erros de compilação. Mas quando o sistema de tipagem é fraco, como no JavaScript, alguns erros são difíceis de detectar.

Um exemplo de uma linguagem de tipagem forte é o TypeScript. Em TypeScript, se você tem uma função que espera numbers como seu argumento, O TypeScript não permitirá que você dê uma string. Ele vai garantir que você dê um number.

TypeScript
const multiply = (a: number, b: number): number => {
  return a * b;
};

multiply('1', 2);
//=> ⚠️ COMPILER ERROR: Argument of type 'string' is not assignable to parameter of type 'number'.

Mas o mesmo código em JavaScript não lhe dará nenhum aviso porque você não pode definir tipos de parâmetros em JavaScript. Como você não pode dizer ao JavaScript que uma função espera numbers, ele não terá problemas em aceitar strings.

Às vezes isso será bom, e seu código funcionará de fato corretamente. Em outros momentos... as coisas podem se comportar de forma inesperada.

JavaScript
const multiply = (a, b) => {
  return a * b;
};

multiply('1', 2);
//=> 2

multiply('hello', 2);
//=> NaN

LinkAnálise de código estático

Finalmente, outras ferramentas ajudam a garantir a qualidade do código, analisando seu código e fornecendo informações valiosas. Suítes como SonarQube, PhpMetrics ou SpotBugs, podem fornecer métricas como relatórios de vulnerabilidade, relatórios de cobertura de testes e feedback sobre dívidas técnicas. Estas verificações são chamadas de análise de código estático.

Além da verificação de tipo, o TypeScript também é capaz de fazer algumas análises de código estático. Por exemplo, ele pode detectar o mau uso do operador delete.

TypeScript
const x = 1;
delete x;
//=> ⚠️ COMPILER ERROR: The operand of a 'delete' operator must be a property reference

Aqui, o TypeScript nos avisa para não utilizarmos o operador de delete em uma variável.

Isto é diferente da verificação de tipo. Neste caso, o TypeScript vê que você está cometendo um erro, mas este erro não é baseado em uma verificação de digitação. Ele se baseia em como o operador delete foi concebido para ser utilizado. É por isso que este exemplo se encaixa na categoria de análise estática, não na categoria de verificação de tipo.

LinkOutros Testes Estáticos

Além dos exemplos acima, existem muitos outros tipos de testes estáticos, como por exemplo:

  • Reviews Informais
  • Walkthroughs
  • Reviews Técnicos
  • Inspeções

A questão é que todas estas práticas envolvem a análise de seu código sem executá-lo.

LinkTestes Unitários

Os testes unitários envolvem testar as menores unidades do seu código. Uma "unidade" geralmente é apenas uma função na programação funcional ou uma classe na programação orientada a objetos, mas pode ser mais do que isso. No final do dia, você decide o que significa uma "unidade" no contexto de seu projeto.

Vejamos um exemplo simples de um teste unitário.

JavaScript
export const sum = (a, b) => {
  return a + b;
};

Dada uma função chamada sum, que leva dois números e retorna sua soma. Poderíamos escrever alguns testes unitários, por exemplo:

  • Chamar sum() com 1 e 2 deve retornar 3.
  • Chamar sum() com -3 e 10 deve retornar 7.

É assim que o código ficaria em um teste unitário de verdade:

JavaScript
import { sum } from './sum.js';

describe('sum()', () => {
  it('should sum two positive numbers', () => {
    const actual = sum(1, 2);
    const expected = 3;
    expect(actual).toEqual(expected);
  });

  it('should sum positive and negative numbers', () => {
    const actual = sum(-3, 10);
    const expected = 7;
    expect(actual).toEqual(expected);
  });
});

Agora que escrevemos nossos testes unitários, podemos executá-los usando um test runner, como o Jasmine ou Jest.

bash
> npx jasmine sum.spec.js

Started
Jasmine started

  sum()
    ✓ should sum two positive numbers
    ✓ should sum positive and negative numbers

2 specs, 0 failures
Finished in 0.011 seconds
Executed 2 of 2 specs SUCCESS in 0.011 sec.

LinkTestes de Integração

Enquanto o teste unitário é sobre testar uma unidade individual isoladamente. Testes de _integração_ é sobre testar se uma combinação de unidades podem funcionar em conjunto.

Para ilustrar, digamos que temos uma função chamada showUserName que retorna o nome de um usuário. Mas esta função utiliza outra função chamada findUserById para realmente encontrar este usuário.

JavaScript
const USERS = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Jane' },
  { id: 3, name: 'Joe' }
];

export const findUserById = (id) => {
  return USERS.find((user) => user.id === id);
};

export const showUserName = (userId) => {
  const userName = findUserById(userId).name;
  return userName;
};

Um teste de integração para estes dois módulos seria algo assim:

JavaScript
import { showUserName } from './show-user-name.js';

describe('Integration between showUserName() and findUserById()', () => {
  it('should return the correct user name', () => {
    const name1 = showUserName(1);
    expect(name1).toEqual('John');

    const name2 = showUserName(2);
    expect(name2).toEqual('Jane');

    const name3 = showUserName(3);
    expect(name3).toEqual('Joe');
  });
});

E assim como nosso teste unitário, podemos fazer nosso teste de integração com Jasmine.

bash
> npx jasmine user.spec.js

Started
Jasmine started

  Integration between showUserName() and findUserById()
    ✓ should return the correct user name

1 spec, 0 failures
Finished in 0.006 seconds
Executed 1 of 1 spec SUCCESS in 0.006 sec.

Este é um exemplo simples. Como eu disse antes, você define o que significa uma "unidade". Podemos tornar isso muito mais complexo do que isso. Podemos escrever testes de integração que simulem solicitações HTTP para um servidor. Você decide, nós só queremos ter certeza de que as unidades podem trabalhar em conjunto.

LinkTestes End-to-End

Agora vamos sair de nosso editor de código e nos imaginar como usuários reais.

Um usuário real não se importará se passarmos o parâmetro errado para uma função, ou se estivermos recebendo os dados corretos de uma chamada API. O usuário só está interessado no que ele realmente pode ver e interagir com ele. E para isso, temos testes de end-to-end, também conhecidos como e2e.

Os testes de end-to-end são sobre testar a interação do usuário final, mas em vez de contratar humanos, podemos usar uma ferramenta que simula nossos usuários.

An end-to-end test runner will run tests against your entire application using the same interface as your end-users. For example, a web application runs in the browser, so your end-to-end test runner should interact with your application using a browser, just like a real user.

Um test runner end-to-end executará testes em toda sua aplicação usando a mesma interface que seus usuários finais. Por exemplo, uma aplicação web roda no navegador, portanto, seu test runner end-to-end deve interagir com sua aplicação usando um navegador, assim como um usuário real.

Cypress é um test runner e2e muito popular e moderno para navegadores. Vejamos um exemplo de teste e2e escrito para a Cypress:

JavaScript
describe('End-to-end testing example', () => {
  it('should have the correct title', () => {
    cy.visit('/');
    const fruits = ['Apple', 'Watermelon', 'Banana', 'Peach', 'Orange'];

    cy.get('.title').should('contain', 'Fruits');

    fruits.forEach((fruit) => {
      cy.get('.fruits li').should('contain', fruit);
    });
  });
});

Neste caso, queremos testar se quando um usuário visitar nossa aplicação de frutas, ele poderá ver o título correto do site e a lista correta de frutas.

Executar este teste não é tão simples como executar testes unitários ou de integração porque, como eu disse antes, os testes end-to-end exigem que sua aplicação esteja em pleno funcionamento. Em nosso caso, nossa aplicação é um servidor de arquivos simples, e podemos iniciá-lo executando npm start.

bash
> npm start

   ┌───────────────────────────────────────────────────┐
   │                                                   │
   │   Serving!                                        │
   │                                                   │
   │   - Local:            http://localhost:3000       │
   │   - On Your Network:  http://192.168.0.108:3000   │
   │                                                   │
   │   Copied local address to clipboard!              │
   │                                                   │
   └───────────────────────────────────────────────────┘

Now that our application is alive, we can run our end-to-end tests against it. Talking specifically about Cypress, there are two ways of running our tests:

Agora que nossa aplicação está viva, podemos executar nossos testes end-to-end nela. Falando especificamente sobre o Cypress, há duas maneiras de executar nossos testes:

  1. Usando o Cypress CLI
  2. Usando o Cypress GUI

A execução dos testes com o Cypress CLI é muito semelhante à forma como temos feito as coisas até agora. Você simplesmente executa a 'cypress run', e ele vai executar os testes e mostrar os resultados em seu terminal.

bash
> npx cypress run

====================================================================================================

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:        9.6.0                                                                          │
  │ Browser:        Electron 94 (headless)                                                         │
  │ Node Version:   v16.14.0 (/Users/lucas/.nvm/versions/node/v16.14.0/bin/node)                   │
  │ Specs:          1 found (fruits.spec.js)                                                       │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘

────────────────────────────────────────────────────────────────────────────────────────────────────

  Running:  fruits.spec.js

  End-to-end testing example
    ✓ should have the correct title (200ms)

  1 passing (243ms)

  (Results)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Tests:        1                                                                                │
  │ Passing:      1                                                                                │
  │ Failing:      0                                                                                │
  │ Pending:      0                                                                                │
  │ Skipped:      0                                                                                │
  │ Screenshots:  0                                                                                │
  │ Video:        true                                                                             │
  │ Duration:     0 seconds                                                                        │
  │ Spec Ran:     fruits.spec.js                                                                   │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘

  (Video)

  -  Started processing:  Compressing to 32 CRF
  -  Finished processing: /Users/lucas/Downloads/type-of-tests-master/end-to-end-test    (0 seconds)
                          s/cypress/videos/fruits.spec.js.mp4

====================================================================================================

  (Run Finished)

       Spec                                              Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  fruits.spec.js                           236ms        1        1        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✔  All specs passed!                        236ms        1        1        -        -        -

Mas, ei, se fosse um verdadeiro humano correndo este teste, você seria capaz de ver a pessoa abrindo o Google Chrome e interagindo. E adivinhe? Cypress permite que você tenha uma experiência semelhante!

A execução de cypress open abrirá uma janela de cypress. Então você pode clicar no arquivo fruits.spec.ts e ver seu navegador rodando seu teste, controlado pelo Cypress.

cypress-gui-running

Muito legal, certo?

É tão legal que você pode pensar que é uma boa idéia esquecer os outros tipos de testes e apenas escrever testes end-to-end. E não vou mentir, isso pode funcionar. Mas...

Como você deve ter notado, os testes end-to-end levam muuuito mais tempo que os testes unitários e de integração. Este teste simples levou 236ms usando o Cypress CLI. Imagine centenas destes testes. Você rapidamente entrará em um estado em que leva uma hora para executar todos os testes e2e.

To work around that, most end-to-end test runners allow you to throw money at the problem and run your tests in parallel. But it will never be as fast as running unit and integration tests. This means that your developers will be annoyed at how long it takes, and they will eventually stop running the tests locally.

Para contornar isso, a maioria dos test runners end-to-end permitem que você jogue dinheiro no problema e execute seus testes em paralelo. Mas nunca será tão rápido quanto os testes de unidade e de integração. Isto significa que seus desenvolvedores ficarão aborrecidos com o tempo que leva, e eles eventualmente deixarão de executar os testes localmente.

Além disso, os testes end-to-end exigem toda a configuração para executar de fato toda a sua aplicação. O que é mais uma barreira para seus desenvolvedores.

LinkVeredito

Portanto, aqui está meu veredito pessoal, e você pode ignorá-lo se discordar:

  1. Acho que deveríamos ter convenções de estilo e usar testes estáticos para cumprí-las. Isto economizará horas debatendo se um novo projeto deve usar abas ou espaços.
  2. Acho que deveríamos ter testes rápidos de unidade e integração que funcionem cada vez que modificarmos um arquivo. Dessa forma, podemos ter um feedback rápido se nosso código quebrar alguma coisa.
  3. E por último, acho que deveríamos ter testes end-to-end, particularmente para as principais funcionalidades de nossa aplicação. Não importa quantas unidades e testes de integração eu tenha, testes end-to-end são necessários. Pessoalmente, eu não me sentiria confiante ao lançar uma aplicação sem executar primeiro alguns testes end-to-end.

LinkNão pare aqui

Se você quiser mergulhar mais fundo nos testes de software ou nas tecnologias utilizadas neste artigo, como JavaScript, TypeScript e Cypress, considere se inscrever na nossa newsletter. É livre de spam. Mantemos os e-mails curtos e valiosos. Se você preferir assistir nosso conteúdo, também temos um Canal no YouTube que você pode se inscrever.

E se sua empresa está procurando por desenvolvedores web remotos, considere contatar minha equipe e eu. Você pode fazer isso através de email, Twitter, or Instagram.

Tenha um ótimo dia!

– Lucas

LinkReferências

  1. Repositório do GitHub com os exemplos
  2. Teste Estático vs Unitário vs Integração vs E2E para Aplicações Frontend
  3. 20 Tipos de Testes que Todo Desenvolvedor Deveria Saber - Semaphore
  4. Lint Code: O que é Linting + Quando Usar Ferramentas de Linting | Perforce.
  5. Tipos de testes de software: Diferentes tipos de testes com detalhes
  6. O que é teste de unidade? | Autify Blog
  7. Testes de integração (com exemplos) | por Team Merlin | Serviços Digitais do Governo, Cingapura
  8. Tipos de teste de software | A lista completa | Edureka
  9. Análise de código estático no JavaScript & Ferramentas de Segurança de Review | SonarQube
  10. Como fazer testes End-to-End
  11. o que é teste End-To-End: Framework de testes E2E com Exemplos
  12. Regra no-var do ESLint
  13. Convenções de estilo do Airbnb
  14. Convenções de estilo do Facebook para projetos JavaScript

Assine a nossa Newsletter e seja avisado quando eu lançar um curso, postar um vídeo ou escrever um artigo.

Campo obrigatório
Campo obrigatório