DEV Community

Cover image for Desafios Comuns na Escrita de Testes Automatizados: Rumo à Clareza e Padronização - Parte 3
Ricardo Silva
Ricardo Silva

Posted on

Desafios Comuns na Escrita de Testes Automatizados: Rumo à Clareza e Padronização - Parte 3

Na parte dois, nós vimos as três etapas que compõem um teste automatizado: Preparação, execução e resultado.

Com isso em mente, vamos dedicar nossa terceira parte a responder às seguintes perguntas:

  • O que devo testar?
  • O que não devo testar?

O que devo testar?

Apesar de ser uma atividade técnica, o ato de testar é influenciado por alguns fatores subjetivos que podem dificultar a vida de uma pessoa na hora de definir o escopo que precisa ser testado. Cada membro pode ter sua própria interpretação do que é importante testar, e isso é um fator subjetivo. Determinadas partes do sistema têm mais criticidade que outras, e isso é outro fator subjetivo.

Antes de mais nada, o time precisa ter discussões claras que irão colocar todos na mesma página a respeito do que é importante testar, pois estabelecendo esse tipo de acordo, ficará mais fácil identificar quando determinada entrega (um PR, por exemplo) contém testes que fogem muito da estrutura dos demais testes existentes no projeto.

Eu costumo propor que os testes precisam usar como referência principal os requisitos da tarefa, levando em conta os cenários "felizes" e "não felizes", onde o caminho feliz é o percurso ideal de execução, seguindo todas as condições esperadas sem ocorrência de erros, contrastando com o caminho não feliz, que abrange situações indesejadas ou desvios do comportamento previsto.

Se essas informações não estão claras na descrição da tarefa, é importante mapeá-los (ao menos os iniciais, uma vez que dúvidas e requisitos não identificados antes poderão aparecer durante o processo de desenvolvimento).

Vamos considerar que a seguinte task foi criada no board do projeto:

Cadastro de Chamado no Sistema

História:
Como usuário do sistema, desejo poder cadastrar um chamado para reportar problemas ou solicitar suporte.

Critérios de Aceitação:

  1. Número Único de Identificação:
    • Após o cadastro bem-sucedido, com usuário informando título e descrião do chamado, o sistema deve gerar automaticamente um número único de identificação para o chamado.
    • Número único de identificação precisa conter letras maísculas e números

Cenários de Teste:

  • Caminho Feliz:
    • O sistema gera um número único de identificação para o chamado.
  • Caminho Não Feliz:
    • O usuário tenta cadastrar um chamado sem preencher todos os campos obrigatórios, resultando em uma mensagem de erro.

Vamos supor que estamos na etapa de construir um Service Object que aplicará essas regras descritas na tarefa. Um bom ponto de partida seria “transferir os requisitos” da tarefa para uma estrutura mais ou menos assim:

# ticket_creator_spec.rb

RSpec.describe TicketCreator do
  describe '#call' do
    context 'When title and description are filled in' do
      it 'generates a unique identification number with uppercase letters and numbers' do
        # etapas do caminho feliz ficarão aqui
      end
    end

    context 'When title is not filled in' do
      it 'raises an error' do
        #etapas do caminho não feliz ficarão aqui
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

O próximo passo é focar em escrever as etapas dos testes. Vamos começar pelo caminho feliz?

Podemos dar início através do setup do teste, sempre tentando montar essa etapa somente com o que é estritamente necessário. Nesse caso, seriam os dados de título e descrição.

# ticket_creator_spec.rb

RSpec.describe TicketCreator do
  describe '#call' do
    context 'When title and description are filled in' do
      it 'generates a unique identification number with uppercase letters and numbers' do
         valid_attributes = { 
                   title: 'Issue Title', 
                     description: 'Detailed description of the issue' 
                 }
      end
    end

    context 'When title is not filled in' do
      it 'raises an error' do
        #etapas do caminho não feliz ficarão aqui
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Como eu considero que isso é muito pouco para executar o teste, vou seguir com o segundo passo, que seria a execução, colocando em nosso teste a unidade de código testada.

# ticket_creator_spec.rb

RSpec.describe TicketCreator do
  describe '#call' do
    context 'When title and description are filled in' do
      it 'generates a unique identification number with uppercase letters and numbers' do
         valid_attributes = { 
                   title: 'Issue Title', 
                     description: 'Detailed description of the issue' 
                 }

                ticket = TicketCreator.new(valid_attributes).call
      end
    end

    context 'When title is not filled in' do
      it 'raises an error' do
        #etapas do caminho não feliz ficarão aqui
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Aqui já conseguimos ter duas escolhas:

  • Rodar o teste e receber o feedback de que a classe TicketCreator ainda não existe, mais ou menos assim:

    NameError:
      uninitialized constant TicketCreator
    
  • Partir para a terceira etapa, que seria determinar que após a execução da unidade de código (TicketCreator), um identification_number precisa ter sido gerado

Se a ideia for seguir pelo segundo caminho, nosso teste poderia ficar mais ou menos assim:

# ticket_creator_spec.rb

RSpec.describe TicketCreator do
  describe '#call' do
    context 'When title and description are filled in' do
      it 'generates a unique identification number with uppercase letters and numbers' do
         valid_attributes = { 
                   title: 'Issue Title', 
                     description: 'Detailed description of the issue' 
                 }

                ticket = TicketCreator.new(valid_attributes).call

                expect(ticket.identification_number).not_to be_nil
                expect(ticket.identification_number).to match(/\A[A-Z0-9]+\z/)
      end
    end

    context 'When title is not filled in' do
      it 'raises an error' do
        #etapas do caminho não feliz ficarão aqui
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Na medida em que formos rodando os testes, coletando os feedbacks e implementando o código até que os testes passem
(ciclo Red-Green-Refactor), no final teremos uma classe mais ou menos assim:

# app/services/ticket_creator.rb

class TicketCreator
  attr_reader :title, :description

  def initialize(attributes = {})
    @title = attributes[:title]
    @description = attributes[:description]
  end

  def call
    ticket = Ticket.create(
      title: title,
      description: description,
      identification_number: generate_identification_number
    )

    ticket
  end

  private

  def generate_identification_number
    SecureRandom.hex(6).upcase
  end
end
Enter fullscreen mode Exit fullscreen mode

Uma vez que o caminho feliz foi implementado, podemos partir para a implementação do cenário não feliz com a confiança de que, caso façamos besteira e o caminho feliz pare de funcionar, seremos devidamente advertidos pelos testes.

Primeiro os testes

# ticket_creator_spec.rb

RSpec.describe TicketCreator do
  describe '#call' do
    context 'when title and description are filled in' do
      it 'generates a unique identification number with uppercase letters and numbers' do
         valid_attributes = { 
                   title: 'Issue Title', 
                     description: 'Detailed description of the issue' 
                 }

                ticket = TicketCreator.new(valid_attributes).call

                expect(ticket.identification_number).not_to be_nil
                expect(ticket.identification_number).to match(/\A[A-Z0-9]+\z/)
      end
    end

    context 'when title is not filled in' do
      it 'raises an error' do
        invalid_attributes = { 
          title: '', 
          description: 'Detailed description of the issue' 
        }

        expect { TicketCreator.new(invalid_attributes).call }.to raise_error(ValidationError, 'Title can\'t be blank')
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Agora a implementação

# app/services/ticket_creator.rb

class TicketCreator
  attr_reader :title, :description

  def initialize(attributes = {})
    @title = attributes[:title]
    @description = attributes[:description]
  end

  def call
    validate_attributes

    ticket = Ticket.create(
      title: title,
      description: description,
      identification_number: generate_identification_number
    )

    ticket
  end

  private

  def validate_attributes
    raise ValidationError, 'Title can\'t be blank' if title.blank?
  end

  def generate_identification_number
    SecureRandom.hex(6).upcase
  end
end

Enter fullscreen mode Exit fullscreen mode

Obviamente, o código final da classe irá variar de acordo com o gosto da pessoa/time que estiver desenvolvendo, mas o importante aqui é que, desde o início, o foco sempre foi atender aos requisitos determinados na tarefa e avançar em direção a um código legível e organizado, uma prova de que os testes têm influência direta no design do código implementado, ao mesmo tempo em que diminui os riscos de se gerar novos bugs durante o processo.

Outro ponto que vale destacar é a relevância dos dados de entrada das classes em relação aos dados de saída. Através dos dados de entrada, nós conseguimos ter total controle a respeito dos resultados esperados, baseado nos contextos definidos. É por isso que passar um título preenchido ou não foi o suficiente para simular o caminho feliz e o caminho não feliz.

O que não devo testar?

Existe um motivo para que o método call seja o único método levado em consideração nos testes: métodos privados são detalhes de implementação e não devem ser testados diretamente, ao mesmo passo em que, se uma classe tem somente um método público, isso será uma forma de garantir que ela assumirá somente uma responsabilidade (aqui não é sobre testes necessariamente, mas sobre o princípio de responsabilidade única)

O foco do teste precisa ser garantir que o comportamento externo da unidade de código (a classe TicketCreator) atenda aos requisitos especificados. E isso acontece através dos métodos públicos, que são de quem os usuários e outros componentes dependerão.

Testar métodos privados pode (e provavelmente vai) resultar em testes frágeis e poluídos, uma vez que estarão sujeitos a mudanças internas na implementação que não afetarão o comportamento externo da unidade de código testada, ou seja: a saída do método público. Isso quer dizer que mudanças nos métodos privados poderão ou não causar quebra ou mudança no comportamento da classe, mas caso cause, os testes do método público já estarão cobrindo isso.

Se um método privado executa rotinas muito complexas, vale a pena extrair esse código para uma classe contendo seus próprios testes.

Testes simples, implementação simples… De complicado já basta a vida, não é verdade?

E assim finalizamos a parte três da nossa série.

Nos vemos na parte quatro!

Top comments (0)