DEV Community

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

Posted on

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

Do que é composto um software?

Gosto da premissa de que um software pode ser considerado um conjunto de unidades de código, como objetos, classes, métodos e variáveis, que interagem e se comunicam entre si para realizar determinadas operações.

O que faz um código ser considerado um código de qualidade?

Em poucas palavras, é quando essas unidades de código conseguem existir de forma coesa (onde tudo faz sentido) e desacoplada (quando a alteração de uma unidade não causa a quebra de uma outra unidade).

Nos próximos artigos nós tentaremos entender um pouco mais sobre como os testes automatizados, desde que bem escritos, podem nos ajudar a conseguir colocar em prática os pontos acima.

ps: A ideia aqui não é ensinar os conceitos básicos de testes automatizados, muito menos mergulhar nas sopas de letrinhas do SOLID, BDD, DRY, Design Patterns, pirâmide de testes, etc. Vou evitar até mesmo usar termos como testes de integração e unitários, utilizando sempre o termo “testes automatizados". Embora tudo isso seja levando em conta no conteúdo proposto (até porque nada do que vou passar aqui foi inventado por mim), a ideia principal é estabelecer um ponto de partida para criar testes automatizados que sejam consistentes e que validem de fato o que um software deve fazer.

ps2: Os conceitos e exemplos aqui são agnósticos a tecnologias. Irei utilizar Ruby (com Rails) e RSpec como stack para os exemplos, mas recomendo que tente replica-los na stack que você preferir.

ps3: Como o óbvio também precisa ser dito, objetivo aqui não é determinar a melhor forma de escrever testes e sim tentar estabelecer algumas formas padronizadas de fazer isso.

Nessa primeira parte nós começaremos abordando:

  • Consequências ao ignorar os testes automatizados
  • Testes automatizados como documentação

> Consequências ao ignorar os testes automatizados

Algo que vi com certa frequência em boa parte dos projetos em que trabalhei foi a falta de clareza e objetividade na escrita dos testes automatizados. Não raramente, desenvolvedores dos mais diversos níveis de experiência apresentando insegurança a respeito de como escrever testes automatizados para um código que ele mesmo escreveu me faz refletir sobre como algum tipo de padronização de pensamento poderia ajudar na hora de montar esses testes.

Em resumo:

1 - Nenhum desenvolvedor (com o juízo no lugar) questiona a importância de escrever testes.

2 - Muitos desenvolvedores não tem clareza sobre como escrever tais testes.

A falta de clareza sobre como escrever testes leva muitos desenvolvedores a gastar horas tentando reproduzir cenários para validar implementações ou corrigir bugs usando ferramentas como Postman ou o console do framework.

Agora vamos ao nosso primeiro exemplo, já que ninguém é obrigado a acreditar no que eu falo:

Em nosso exemplo, nós temos uma API responsável por publicar artigos, sendo composto pelos seguintes requisitos:

  • Article pertence à Author e Category, que precisam estar previamente cadastrados
  • Article possui title e body como atributos
  • Ao cadastrar um novo artigo, o autor precisará ser notificado que seu artigo já foi publicado
  • Teremos um Service Object responsável por executar todo esse fluxo

Dito isso, em um cenário onde testes automatizados estão sendo ignorados naquele momento pelo desenvolvedor, ele precisará abrir o console e preparar tudo o que precisa para reproduzir o fluxo de publicação de um artigo:

# primeiro criando um autor
author = Author.create(name: 'John Doe', email: 'john.doe@example.com')

# depois criando uma categoria
category = Category.create(name: 'Technology')

# em seguida montando o hash com os dados de um novo artigo
article_params = {
  title: 'Introduction to Rails',
  body: 'This is the content of the article.',
  author_id: author.id,
  category_id: category.id
}

# e executando o Service Object que irá performar todo o fluxo de criação 
service = ArticleCreationService.new(article_params)
service.call
Enter fullscreen mode Exit fullscreen mode

Quando eu disse que os testes foram ignorados pelo desenvolvedor, eu me referi a duas possibilidades bem comuns:

  • Os testes não existem e o desenvolvedor optou por escreve-los somente após concluir a implementação em que está trabalhando (lembrando que pode ser um bugfix, refatoração ou algo totalmente novo).

  • Os testes existem, mas o desenvolvedor não quis abri-los, entende-los e executá-los.

Qualquer uma das opções impactam negativamente em vários aspectos, dos quais nesse momento podemos destacar duas:

  • Entender o código de um Service Object pode ser demorado, pois o desenvolvedor precisa analisar o código-fonte e as classes relacionadas para compreender completamente o propósito da implementação. Até que ocorram "epifanias" que esclareçam o fluxo da execução, o desenvolvimento pode ser prolongado, especialmente ao lidar com código legado.

  • A montagem e reprodução do cenário necessário para testes também podem ser morosas. No exemplo fornecido, já foi necessário criar manualmente registros no banco e preparar um hash de dados para um novo artigo. Em um software real com mais complexidade, essa preparação pode envolver várias etapas, com vários possíveis contextos e resultados diferentes, tornando frequentemente necessário o auxílio de um membro experiente da equipe para configurar o ambiente de teste, ao contrário da facilidade proporcionada por comandos que automatizam esse processo.

> Testes automatizados como documentação

Testes automatizados, antes de mais nada, são uma documentação viva do software, servindo de referencia para o entendimento de contextos de cada funcionalidade, contemplando as entradas esperadas, saídas esperadas, mudanças implementadas e a relação entre cada uma das unidades de que compõem de código de um software.

Agora um exemplo de testes que poderiam auxiliar o desenvolvedor nesse primeiro desafio de entender e reproduzir o cenário desejado:

# spec/services/article_creation_service_spec.rb
require 'rails_helper'

RSpec.describe ArticleCreationService, type: :service do
  describe '#call' do
    context 'when passing valid data' do
      it 'creates a new article' do
                author = create(:author)
        category = create(:category)
        article_params = {
          title: 'Test Article',
          body: 'This is a test article.',
          author_id: author.id,
          category_id: category.id
        }

        ArticleCreationService.new(article_params).call

        expect(Article.count).to eq(1)
      end

      it 'enqueues email notification job' do
                author = create(:author)
        category = create(:category)
        article_params = {
          title: 'Test Article',
          body: 'This is a test article.',
          author_id: author.id,
          category_id: category.id
        }

        ArticleCreationService.new(article_params).call

        expect(Sidekiq::Worker.jobs.count).to eq(1)
        expect(Sidekiq::Worker.jobs.first['class']).to eq('ArticleMailer')
      end
    end

    context 'when passing invalid data' do
      it 'does not create a new article' do
                invalid_article_params = {
          title: 'Test Article',
          body: 'This is a test article.',
          author_id: nil,
          category_id: nil
        }

        ArticleCreationService.new(invalid_article_params).call

        expect(Article.count).to eq(0)
      end

      it 'does not enqueue email notification job' do
                invalid_article_params = {
          title: 'Test Article',
          body: 'This is a test article.',
          author_id: nil,
          category_id: nil
        }

        ArticleCreationService.new(invalid_article_params).call

        expect(Sidekiq::Worker.jobs.count).to eq(0)
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Essa estrutura é focada em três blocos: describes, contexts e its

O bloco describe determina qual a unidade de código está sendo testada, no nosso caso aqui estamos falando do método call. O describe engloba todos os contexts (possível contextos), que por sua vez guardam os its que são as saídas esperadas para essas unidades de código.

O termo “documentação viva” se dá justamente por conta da mensagem que os testes passam, descrevendo o que está sendo testado e qual o comportamento esperado de acordo com cada possível cenário:

**Dada(describe)** a unidade de código ArticleCreationService, 
  **Que contém (describe)** a unidade de código call
    **Quando (context)** forem passados dados válidos
      **Espera-se (it)** que um novo artigo seja criado
      **Espera-se (it)** que um email de notificação seja enfileirado
    **Quando (context)** forem passados dados inválidos
      **Espera-se (it)** que um novo artigo não seja criado
      **Espera-se (it)** que um email de notificação não seja enfileirado
Enter fullscreen mode Exit fullscreen mode

Outro ponto que faz com que testes sejam uma documentação viva é que alterações no código poderão acarretar em quebra dos testes, o que forçará o desenvolvedor a ajustar os testes de acordo com a nova necessidade e por consequência os descritivos de cada bloco.

Por enquanto é isso. Nas próximas continuações (se a procrastinação não me vencer) iremos ver outras abordagens que te ajudarão na hora de escrever testes para suas aplicações.

Até lá.

Top comments (0)