Fala, Guerreiros e Guerreiras!

Nesta tarde, ao som da maior banda de death/black metal do Brasil e do mundo, o lendário Sarcófago, de Minas Gerais! Na agulha, The Laws of Scourge. Depois, foi a vez do Sextrash, com Sexual Carnage.

Vamos falar de um assunto que nem sempre recebe a atenção que merece no desenvolvimento de software: testes automatizados, cobertura de código, análise estática e segurança em aplicações Ruby on Rails.

É comum encontrar projetos que funcionam aparentemente bem, mas não possuem uma suíte de testes consistente. Também existem aplicações com centenas de testes que passam no ambiente local, mas apresentam falhas quando recebem dados inesperados, executam processos em segundo plano ou precisam lidar com regras de negócio mais complexas.

E existe outro problema: confundir quantidade de testes com qualidade de software.

Ter 90% de cobertura não significa que uma aplicação esteja 90% livre de bugs. Executar um linter sem erros não garante que a arquitetura esteja bem construída. Passar por uma análise de segurança não significa que todas as vulnerabilidades foram eliminadas. E atualizar as dependências não garante, por si só, que a aplicação continuará funcionando corretamente.

Cada ferramenta responde a perguntas diferentes. O trabalho de engenharia está em compreender essas diferenças e combiná-las de forma inteligente.

Neste artigo, vamos trabalhar com ferramentas bastante conhecidas do ecossistema Ruby:

  • RSpec: testes automatizados;
  • FactoryBot e fixtures: preparação de dados de teste;
  • Database Cleaner e transações: isolamento do banco de dados;
  • SimpleCov: cobertura de código;
  • RuboCop: análise estática e padrões de código;
  • Brakeman: análise de segurança para aplicações Rails;
  • Bundler Audit: verificação de vulnerabilidades conhecidas nas dependências Ruby;
  • CI/CD: automação dessas verificações durante o desenvolvimento.

A proposta não é apenas apresentar comandos de terminal. É entender o que cada ferramenta verifica, o que ela não verifica e como interpretar seus resultados.

1. RSpec: por que testar antes de colocar em produção?

O RSpec é uma das principais ferramentas de testes automatizados no ecossistema Ruby. Em aplicações Rails, o rspec-rails integra essa estrutura ao framework.

Em vez de depender exclusivamente de testes manuais, podemos descrever comportamentos esperados e executá-los sempre que modificamos o sistema.

Imagine um sistema comercial com uma regra simples:

Um pedido não pode ser confirmado se não possuir itens.

Se essa regra estiver implementada no modelo, em um serviço ou em outro componente, precisamos de um teste que comprove o comportamento esperado.

Exemplo simplificado:

RSpec.describe Order, type: :model do
  describe "#confirm!" do
    it "does not confirm an order without items" do
      order = build(:order)

      expect {
        order.confirm!
      }.to raise_error(ActiveRecord::RecordInvalid)
    end
  end
end

Esse exemplo pressupõe que a factory :order exista e que a implementação de confirm! levante essa exceção. Em um projeto real, o teste deve refletir o contrato efetivamente definido pela aplicação.

O ponto central é que o teste registra uma expectativa verificável. Se uma alteração futura permitir que pedidos vazios sejam confirmados, a suíte poderá identificar a regressão.

1.1. Executando os testes

Para executar a suíte:

bundle exec rspec

O bundle exec executa o comando usando as gems resolvidas pelo Bundler para o projeto.

Uma opção bastante útil durante o desenvolvimento é:

bundle exec rspec --format documentation

O parâmetro --format documentation apresenta os grupos e exemplos pelos seus nomes, em vez de mostrar apenas uma sequência de pontos e falhas.

Uma saída ilustrativa seria:

Order
  #confirm!
    does not confirm an order without items

Finished in 0.12 seconds
1 example, 0 failures

Essa leitura facilita a identificação do comportamento executado e ajuda a transformar a própria suíte em uma documentação viva das regras do sistema.

1.2. Flags úteis do RSpec

Executar um arquivo específico:

bundle exec rspec spec/models/order_spec.rb

Útil quando estamos desenvolvendo ou corrigindo uma funcionalidade localizada.

Executar exemplos que correspondem a uma descrição:

bundle exec rspec --example "confirm an order"

O filtro procura exemplos que correspondam à descrição. Dependendo da suíte, pode executar mais de um teste.

Interromper a execução na primeira falha:

bundle exec rspec --fail-fast

Ajuda durante a investigação de falhas, embora não apresente todos os problemas em uma única execução.

Usar uma semente específica para reproduzir a ordem dos testes:

bundle exec rspec --seed 12345

É útil quando a suíte utiliza execução aleatória e um teste depende indevidamente do estado deixado por outro.

Identificar exemplos lentos:

bundle exec rspec --profile 10

Apresenta informações sobre os dez exemplos mais lentos, facilitando a investigação de gargalos.

Combinar opções:

bundle exec rspec --format documentation --fail-fast

Para conhecer todas as opções da versão instalada:

bundle exec rspec --help

Documentação: RSpec Rails. Curso: Recomendo o mestre Jackson Pires.

2. Como organizar os testes em um projeto Rails

Antes de escrever testes, precisamos entender a estrutura que o RSpec utiliza dentro de uma aplicação Rails.

Uma organização comum é:

meu_projeto/
├── app/
│   ├── controllers/
│   ├── models/
│   ├── services/
│   └── jobs/
├── config/
│   └── database.yml
├── spec/
│   ├── factories/
│   │   ├── users.rb
│   │   ├── products.rb
│   │   └── orders.rb
│   ├── models/
│   │   ├── user_spec.rb
│   │   └── product_spec.rb
│   ├── requests/
│   │   ├── products_spec.rb
│   │   └── orders_spec.rb
│   ├── services/
│   │   └── order_service_spec.rb
│   ├── jobs/
│   │   └── order_notification_job_spec.rb
│   ├── system/
│   │   └── checkout_spec.rb
│   ├── support/
│   │   ├── authentication_helpers.rb
│   │   └── request_helpers.rb
│   ├── rails_helper.rb
│   └── spec_helper.rb
├── .rspec
├── Gemfile
└── Gemfile.lock

Essa estrutura é ilustrativa. Nem todo projeto precisa de todos esses diretórios. A organização deve acompanhar as necessidades reais da aplicação.

2.1. spec_helper.rb: configuração geral do RSpec

O spec_helper.rb guarda configurações gerais do RSpec que não dependem necessariamente do Rails.

Por exemplo:

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
end

A opção verify_partial_doubles ajuda a identificar stubs e expectativas incompatíveis com os métodos reais dos objetos.

Isso reduz o risco de criar testes que passam porque um mock foi configurado para um método inexistente ou com uma interface incorreta.

O spec_helper.rb também pode conter configurações de formatação, ordem de execução, perfilamento e outras opções gerais.

2.2. rails_helper.rb: preparando o ambiente Rails

Já o rails_helper.rb prepara o ambiente da aplicação para os testes.

Normalmente, ele carrega o Rails, inicializa as configurações necessárias e importa o spec_helper.rb.

Um exemplo simplificado:

ENV["RAILS_ENV"] ||= "test"

require_relative "../config/environment"
abort("Rails está em produção!") if Rails.env.production?

require "rspec/rails"

Esse trecho é ilustrativo. A estrutura gerada pelo rspec-rails pode conter outras configurações importantes, como a definição dos diretórios de fixtures e o comportamento das transações de teste.

A distinção entre os arquivos é simples:

  • spec_helper.rb: configuração geral do RSpec;
  • rails_helper.rb: integração com o ambiente Rails.

Os testes de modelos, requisições e outras partes da aplicação geralmente carregam o rails_helper.rb.

2.3. Arquivo .rspec

O arquivo .rspec permite definir opções padrão para executar a suíte.

Por exemplo:

--require spec_helper
--format progress

Com isso, o RSpec carrega o helper indicado e utiliza o formato de saída escolhido.

Se quisermos uma saída mais descritiva em determinada execução, podemos sobrescrever o formato:

bundle exec rspec --format documentation

É importante não carregar indiscriminadamente helpers que dependem do Rails em testes planejados para rodar sem inicializar a aplicação.

2.4. Arquivos *_spec.rb

Por convenção, os testes do RSpec costumam terminar com _spec.rb.

Exemplos:

  • spec/models/product_spec.rb;
  • spec/requests/orders_spec.rb;
  • spec/services/order_service_spec.rb;
  • spec/jobs/order_notification_job_spec.rb.

Essa convenção permite ao RSpec localizar os exemplos automaticamente e ajuda a equipe a descobrir onde estão os testes de determinado componente.

3. Factories, fixtures e seeds: preparando dados de teste

Os testes precisam de dados previsíveis. Se cada exemplo criar registros de uma maneira diferente, a suíte ficará mais difícil de manter.

Três conceitos aparecem frequentemente nesse contexto: factories, fixtures e seeds.

3.1. FactoryBot: criando objetos de teste

FactoryBot permite definir formas reutilizáveis de construir objetos.

Imagine que vários testes precisem criar um produto:

FactoryBot.define do
  factory :product do
    name { "Teclado" }
    price { 100.00 }
    stock_quantity { 10 }
  end
end

A factory deve refletir os atributos e as regras reais do modelo. O exemplo pressupõe que esses atributos existam na aplicação.

Podemos utilizá-la assim:

product = build(:product)

Ou:

product = create(:product)

A diferença é importante:

  • build(:product) constrói o objeto em memória, sem persistir o registro no banco;
  • create(:product) cria e persiste o registro, normalmente utilizando o banco de dados de teste.

Se o teste exige verificar consultas, associações persistidas ou restrições do banco, create pode ser necessário.

Se o teste verifica apenas uma validação simples, build pode ser suficiente e mais rápido.

3.2. Traits e cenários diferentes

As factories também podem representar variações de um objeto:

FactoryBot.define do
  factory :user do
    name { "Usuário de Teste" }
    email { "[email protected]" }

    trait :admin do
      role { "admin" }
    end
  end
end

Uso:

user = create(:user, :admin)

Assim, evitamos manter várias factories quase idênticas para representar pequenas diferenças de comportamento.

Entretanto, factories não devem esconder regras complexas nem criar automaticamente um grande conjunto de registros que o teste não precisa. Quanto mais explícitos forem os dados, mais fácil será compreender o cenário.

3.3. Fixtures

Fixtures são conjuntos de dados de teste definidos previamente, tradicionalmente em arquivos YAML no Rails.

Exemplo:

# test/fixtures/users.yml
admin:
  name: Administrador
  email: [email protected]

O Rails possui um sistema nativo de fixtures. Um projeto RSpec pode utilizá-las, dependendo da configuração.

Elas podem ser úteis quando os dados são estáveis e compartilhados entre diversos cenários. Por outro lado, muitas fixtures interdependentes podem dificultar a compreensão de quais registros estão disponíveis em cada teste.

Não é obrigatório utilizar fixtures e factories simultaneamente.

3.4. Seeds

Seeds são dados iniciais da aplicação, definidos normalmente em db/seeds.rb.

Podem ser usados para preparar ambientes de desenvolvimento, demonstração ou outros cenários controlados. Não devem ser confundidos com a estratégia habitual de preparação e isolamento de dados de uma suíte de testes.

Uma regra importante: nunca dependa dos dados que existem no banco de desenvolvimento ou de produção para que os testes funcionem.

Os dados de teste devem ser previsíveis, reproduzíveis e separados dos dados reais dos usuários.

4. Database Cleaner: por que precisamos isolar o banco de dados?

Imagine que um teste crie um usuário com determinado e-mail. Em seguida, outro teste tenta criar um usuário com o mesmo e-mail, mas o primeiro registro continua no banco.

O segundo teste pode falhar por causa de uma restrição de unicidade, mesmo que funcione corretamente quando executado sozinho.

Esse é um exemplo de teste contaminado pelo estado deixado por outro teste.

A limpeza e o isolamento do banco ajudam a evitar esse tipo de problema.

4.1. O Rails já possui mecanismos de limpeza

Em aplicações Rails com RSpec, é comum utilizar as transações de teste fornecidas pela integração com o Active Record.

Por isso, Database Cleaner não é obrigatório em todo projeto Rails.

Antes de adicionar outra dependência, precisamos entender se a estratégia padrão já resolve a necessidade da aplicação.

Database Cleaner pode ser útil em configurações específicas, especialmente quando os testes envolvem múltiplas conexões, processos externos ou situações em que as transações tradicionais não são suficientes.

4.2. Instalando Database Cleaner

Caso o projeto realmente precise da gem, podemos adicioná-la ao Gemfile:

group :test do
  gem "database_cleaner-active_record"
end

Depois:

bundle install

4.3. Exemplo de configuração

Uma configuração possível no rails_helper.rb é:

require "database_cleaner/active_record"

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.start
  end

  config.before(:each, :truncation) do
    DatabaseCleaner.strategy = :truncation
  end

  config.append_after(:each) do
    DatabaseCleaner.clean
  end
end

Esse exemplo ilustra uma estratégia possível, mas não deve ser copiado indiscriminadamente para qualquer projeto. A configuração precisa ser validada de acordo com a versão da gem, o adaptador do banco e o modo de execução dos testes.

Observe também que a configuração acima seleciona a estratégia :truncation para exemplos marcados com :truncation, mas o start ocorre em um before(:each) separado. Em projetos reais, é importante organizar os hooks para garantir que a estratégia seja selecionada antes de iniciar a limpeza de cada exemplo.

Uma alternativa mais explícita é:

require "database_cleaner/active_record"

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :truncation) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.append_after(:each) do
    DatabaseCleaner.clean
  end
end

Antes de adotar esse exemplo, verifique se não existem outras configurações de limpeza em rails_helper.rb, spec/support/ ou em gems do projeto. Não devemos manter mecanismos concorrentes de limpeza sem entender como interagem.

A estratégia de truncation também precisa ser avaliada com cuidado em bancos compartilhados, pois pode remover registros além dos esperados se o ambiente estiver configurado incorretamente.

4.4. Transação ou truncation?

Transaction: envolve as operações do teste em uma transação que pode ser revertida ao final. Geralmente é rápida e adequada quando o acesso ao banco acontece dentro da mesma conexão transacional.

Truncation: limpa registros das tabelas por meio de operações de truncamento ou mecanismo equivalente. Pode ser necessária em cenários nos quais a transação não é suficiente, mas tende a ser mais custosa.

Quando há Selenium, Capybara, servidores de teste ou processos que utilizam conexões separadas, precisamos verificar se o isolamento transacional continua funcionando como esperado.

Não existe uma única estratégia correta para todos os ambientes. A escolha depende de como a aplicação acessa o banco durante os testes.

4.5. Testes não devem depender da ordem de execução

Uma suíte saudável deve evitar situações como:

  • um teste depender de um usuário criado por outro;
  • um teste funcionar somente quando executado depois de determinado arquivo;
  • registros antigos alterarem o resultado de um teste;
  • a suíte passar isoladamente, mas falhar quando executada por completo.

Executar os testes com diferentes sementes ajuda a descobrir problemas de ordem:

bundle exec rspec --seed 12345

O objetivo não é limpar o banco indiscriminadamente. É garantir que cada cenário possua o estado necessário para sua execução e não dependa de efeitos colaterais de outros exemplos.

5. before, after, let e let!: preparando cada cenário

O RSpec oferece recursos para preparar os dados de um teste sem repetir a mesma configuração em todos os exemplos.

before

Executa uma preparação antes dos exemplos abrangidos pelo bloco:

RSpec.describe Product, type: :model do
  before do
    @product = build(:product)
  end

  it "has a name" do
    expect(@product.name).not_to be_empty
  end
end

let

Define um valor calculado sob demanda e armazenado para aquele exemplo:

let(:product) { build(:product) }

it "has a name" do
  expect(product.name).not_to be_empty
end

O let normalmente deixa a preparação mais explícita e reduz a necessidade de variáveis de instância.

let!

Cria o valor antes do exemplo, mesmo que ele não seja acessado explicitamente no corpo do teste:

let!(:product) { create(:product) }

Como o registro é persistido, ele precisa respeitar as regras de isolamento e limpeza do banco.

after

Pode executar ações de encerramento após um exemplo, quando necessário.

Entretanto, não devemos usar after para esconder efeitos colaterais que poderiam ser evitados desde o início. RSpec, Rails e as ferramentas de teste já oferecem mecanismos próprios para diversas tarefas de limpeza.

6. Mocks, stubs e doubles: simulando dependências externas

Testar uma integração real com serviços de pagamento, e-mail ou APIs externas em todos os exemplos pode tornar a suíte lenta, instável e dependente de terceiros.

O RSpec permite criar doubles e configurar respostas para dependências.

Exemplo:

gateway = instance_double(PaymentGateway)

allow(gateway).to receive(:charge).and_return(
  success: true
)

Nesse exemplo, instance_double ajuda a verificar a compatibilidade da interface simulada com a classe referenciada, desde que ela possa ser resolvida corretamente pelo RSpec.

Também podemos verificar interações:

expect(gateway).to receive(:charge)

Mas precisamos usar essas ferramentas com critério.

Se o teste simula todos os componentes e verifica apenas chamadas internas, pode terminar validando a implementação atual, e não o comportamento desejado.

Uma estratégia equilibrada combina:

  • testes isolados com doubles quando isso simplifica a verificação;
  • testes de integração para validar a comunicação entre componentes;
  • testes de contrato quando há integrações externas com interfaces definidas;
  • testes de ponta a ponta para os fluxos críticos.

Mocks não substituem a necessidade de comprovar que as partes do sistema realmente funcionam juntas.

7. Cada camada precisa ser testada de maneira adequada

Um erro comum é concentrar praticamente todos os testes em modelos e controllers, deixando serviços, integrações e processos assíncronos sem validação suficiente.

Em uma aplicação Rails, cada componente possui responsabilidades diferentes. Consequentemente, a estratégia de teste também precisa variar.

7.1. Model Specs: modelos e regras de domínio

Os testes de modelo verificam comportamentos relacionados às entidades do sistema, suas validações, associações e regras de negócio implementadas nessa camada.

Exemplos:

  • um produto não pode ter preço negativo;
  • um usuário precisa ter um e-mail válido;
  • um pedido não pode ser confirmado sem os requisitos necessários;
  • uma transição de estado precisa respeitar as regras do domínio.

Exemplo:

RSpec.describe Product, type: :model do
  it "is invalid with a negative price" do
    product = build(:product, price: -1)

    expect(product).not_to be_valid
  end
end

O teste verifica uma regra específica, sem precisar percorrer toda a aplicação.

Não devemos transformar cada método trivial em um teste sem valor. O foco está nos comportamentos relevantes, nas regras e nos cenários que poderiam produzir regressões.

7.2. Request Specs: requisições HTTP e integração com Rails

Request Specs verificam o comportamento da aplicação a partir de uma requisição HTTP.

Podemos testar:

  • códigos de resposta;
  • autenticação e autorização;
  • parâmetros recebidos;
  • persistência dos dados;
  • redirecionamentos;
  • respostas JSON;
  • tratamento de erros.

Exemplo simplificado:

RSpec.describe "Products", type: :request do
  describe "GET /products" do
    it "returns a successful response" do
      get products_path

      expect(response).to have_http_status(:ok)
    end
  end
end

Esse teste pressupõe que a rota products_path exista e esteja acessível no cenário executado.

Ao contrário de um teste isolado de modelo, aqui verificamos uma parte maior do fluxo de requisição do Rails.

7.3. Service Specs: operações que concentram regras de negócio

Muitas aplicações possuem classes de serviço para organizar operações que envolvem várias etapas.

Imagine um serviço responsável por finalizar uma venda:

  1. validar os itens;
  2. verificar o estoque;
  3. calcular os valores;
  4. registrar a venda;
  5. atualizar o estoque;
  6. gerar uma tarefa para emissão fiscal.

Não é necessário concentrar toda essa lógica em um controller.

Um teste de serviço pode verificar o resultado da operação, o tratamento de erros e as interações necessárias com outras partes do sistema.

Também é importante testar falhas: o que acontece se o estoque não for suficiente? E se uma operação externa estiver indisponível?

Os testes devem refletir as garantias reais do sistema, inclusive quando há transações, concorrência ou efeitos externos.

7.4. Job Specs: processamento assíncrono

O Active Job permite executar tarefas em segundo plano, dependendo do adaptador configurado.

Esses processos podem enviar notificações, gerar relatórios, processar arquivos ou integrar sistemas.

Testes de jobs podem verificar:

  • se a tarefa executa a operação esperada;
  • se os argumentos são tratados corretamente;
  • se os erros são tratados conforme a política definida;
  • se o enfileiramento acontece quando esperado.

Testar que um job foi enfileirado não é a mesma coisa que testar se ele realmente executa corretamente. São comportamentos diferentes e podem exigir testes separados.

7.5. System Specs: o fluxo pela perspectiva do usuário

System Specs permitem testar interações mais completas com a aplicação, geralmente por meio de um navegador automatizado.

Um exemplo seria:

  1. usuário acessa o sistema;
  2. realiza login;
  3. abre o cadastro de produtos;
  4. preenche o formulário;
  5. salva os dados;
  6. verifica a mensagem de sucesso.

Esses testes ajudam a identificar problemas de integração entre interface, rotas, controllers, modelos e JavaScript.

Por outro lado, normalmente são mais lentos e exigem um ambiente de execução mais complexo. Não precisamos reproduzir todos os cenários em testes de navegador.

7.6. Outras camadas que merecem atenção

Dependendo da arquitetura, também podemos precisar de testes para:

  • políticas de autorização;
  • mailers;
  • serializers e contratos de APIs;
  • integrações externas;
  • importação e exportação de dados;
  • tarefas agendadas;
  • canais de comunicação em tempo real.

Não existe uma distribuição universal de testes que sirva para todos os sistemas. O critério deve ser o risco, a complexidade e a responsabilidade de cada componente.

8. Capybara e Selenium: quando o teste precisa de um navegador

Em aplicações que possuem interface web, alguns comportamentos não podem ser validados adequadamente apenas com testes de modelo ou requisições.

Capybara oferece uma interface para descrever interações com a aplicação, enquanto Selenium pode controlar um navegador real ou automatizado.

Um cenário pode verificar se um usuário consegue preencher um formulário e concluir uma operação.

Esses testes são úteis para identificar falhas de navegação, formulários, JavaScript e integração da interface com o backend.

Por outro lado, são mais sensíveis ao ambiente e podem consumir mais tempo.

Por isso, a suíte deve reservar testes de navegador para fluxos que realmente precisam dessa validação, mantendo regras simples e numerosas verificações nas camadas mais rápidas.

9. Coverage: o que a cobertura realmente mede?

Chegamos a uma métrica que costuma gerar interpretações equivocadas: a cobertura de código.

No Ruby, uma ferramenta bastante utilizada para gerar relatórios de cobertura é o SimpleCov.

A cobertura ajuda a identificar quais partes do código foram executadas durante os testes instrumentados. Isso permite localizar áreas importantes que não estão sendo exercitadas pela suíte.

9.1. Configurando o SimpleCov

Adicione a gem ao grupo de desenvolvimento e teste, caso ainda não esteja instalada:

group :development, :test do
  gem "simplecov", require: false
end

Depois:

bundle install

No início do spec/spec_helper.rb, antes de carregar os arquivos da aplicação que queremos medir, podemos configurar:

require "simplecov"

SimpleCov.start "rails" do
  enable_coverage :branch

  add_filter "/spec/"

  add_group "Models", "app/models"
  add_group "Controllers", "app/controllers"
  add_group "Services", "app/services"
  add_group "Jobs", "app/jobs"
end

A configuração pressupõe uma versão do SimpleCov compatível com enable_coverage :branch e com o perfil Rails. Ajuste-a à versão instalada no projeto.

O SimpleCov deve ser inicializado antes do carregamento dos arquivos que serão medidos. Se a inicialização ocorrer tarde demais, parte da cobertura poderá não ser registrada corretamente.

Agora execute:

bundle exec rspec

O SimpleCov normalmente gera um relatório no diretório coverage/. O arquivo coverage/index.html pode ser aberto no navegador para examinar os resultados.

9.2. Como interpretar o relatório?

Imagine que o relatório apresente:

Coverage report generated
Line Coverage: 84.2%
Branch Coverage: 71.5%

Esses números são apenas ilustrativos.

Line Coverage — cobertura de linhas

Indica a proporção de linhas executáveis que foram alcançadas durante a execução instrumentada.

Uma linha coberta não significa necessariamente que seu resultado foi validado. O teste pode executar o código sem verificar se o comportamento está correto.

Branch Coverage — cobertura de ramificações

Ajuda a identificar se diferentes caminhos de decisão foram exercitados, como os lados verdadeiro e falso de uma condição.

Considere:

def discount_for(total)
  if total >= 1_000
    0.10
  else
    0.0
  end
end

Se os testes verificarem somente uma compra de R$ 1.500, a condição verdadeira será exercitada. A condição falsa poderá continuar sem teste.

Um segundo cenário, com uma compra de R$ 500, ajuda a validar o outro caminho.

Esse exemplo mostra por que a cobertura de ramificações pode revelar lacunas que a cobertura de linhas não deixa tão evidentes.

9.3. O que procurar no HTML?

Ao abrir o relatório, observe:

  • Arquivos com cobertura baixa: podem conter regras importantes que não estão sendo testadas.
  • Linhas não cobertas: indicam código que não foi executado pela suíte medida.
  • Ramificações não exercitadas: podem revelar condições ou caminhos alternativos sem validação.
  • Grupos por camada: ajudam a entender se a cobertura está concentrada em uma parte do sistema enquanto outras áreas estão negligenciadas.

Não basta olhar o percentual geral. Um projeto pode ter 95% de cobertura e deixar justamente o cálculo financeiro, a autorização ou a integração fiscal sem testes adequados.

O inverso também merece atenção: um arquivo com cobertura baixa pode conter código simples e pouco crítico. O percentual precisa ser interpretado em conjunto com o risco.

9.4. Cobertura não é sinônimo de qualidade

Imagine dois projetos com 90% de cobertura.

No primeiro, os testes verificam resultados, regras de negócio, erros e limites.

No segundo, os testes executam os métodos, mas fazem poucas verificações sobre os resultados.

Os dois podem apresentar percentuais semelhantes, mas oferecem níveis diferentes de confiança.

Por isso, uma boa estratégia combina cobertura com testes significativos, revisão de código, análise estática e validação dos requisitos.

A meta não deve ser apenas executar o código, mas verificar se ele se comporta corretamente.

Documentação: SimpleCov.

10. RuboCop: lint, estilo e problemas estáticos

O RuboCop analisa código Ruby sem precisar executar todos os caminhos da aplicação.

Ele pode identificar inconsistências de estilo, padrões potencialmente problemáticos e violações das regras configuradas para o projeto.

O termo lint costuma ser usado para essa categoria de verificação estática.

Para executar a análise:

bundle exec rubocop

O RuboCop utiliza regras chamadas cops. Elas pertencem a categorias como Layout, Lint, Style, Metrics, Naming e outras, incluindo extensões específicas para Rails quando instaladas e configuradas.

Uma saída hipotética pode indicar:

app/services/order_service.rb:12:5:
C: Style/GuardClause: Use a guard clause.

1 file inspected, 1 offense detected

Isso não significa automaticamente que o sistema está quebrado. Significa que uma regra configurada identificou um padrão que merece análise.

10.1. O que significa rubocop -A?

O comando é:

bundle exec rubocop -A

O parâmetro -A, equivalente a --autocorrect-all, permite aplicar correções automáticas seguras e inseguras.

A diferença é importante:

bundle exec rubocop -a

O -a aplica correções consideradas seguras.

Já:

bundle exec rubocop -A

Também permite correções classificadas como inseguras, que podem alterar o comportamento do programa em determinadas situações.

Por isso, não recomendo executar -A indiscriminadamente sem revisar as alterações produzidas.

Um fluxo mais cuidadoso seria:

bundle exec rubocop
bundle exec rubocop -a
git diff

Depois de analisar as mudanças, podemos decidir se alguma correção adicional justifica o uso de -A.

10.2. Outras flags úteis do RuboCop

Executar somente os arquivos alterados, quando suportado pela versão instalada:

bundle exec rubocop --changed

Executar somente as regras de lint:

bundle exec rubocop --lint

Exibir apenas categorias específicas:

bundle exec rubocop --only Lint,Layout

Verificar quais arquivos serão analisados:

bundle exec rubocop --list-target-files

Consultar as opções disponíveis:

bundle exec rubocop --help

As opções podem variar conforme a versão instalada. Em particular, flags relacionadas a arquivos alterados e saída em diff devem ser conferidas na ajuda da versão do projeto.

10.3. Lint não é revisão de arquitetura

Um código pode obedecer a todas as regras do RuboCop e ainda apresentar acoplamento excessivo, responsabilidades mal distribuídas, consultas ineficientes ou regras de negócio incorretas.

O linter ajuda a estabelecer padrões e detectar problemas estáticos. Ele não substitui testes, revisão técnica ou decisões arquiteturais.

Documentação: RuboCop — CLI Reference.

11. Brakeman: análise de segurança para aplicações Rails

Uma aplicação pode passar em todos os testes funcionais e ainda possuir vulnerabilidades.

Isso acontece porque testes funcionais verificam comportamentos definidos, enquanto uma análise de segurança procura padrões que podem representar riscos, inclusive em caminhos que não foram cobertos pelos testes.

O Brakeman é um analisador estático voltado para aplicações Ruby on Rails. Ele procura padrões potencialmente perigosos em áreas como processamento de entrada, consultas, renderização e outras operações relevantes para a segurança.

Para executar a análise no projeto:

bundle exec brakeman

Para uma saída mais silenciosa:

bundle exec brakeman -q

O -q reduz mensagens informativas, mas não significa que os alertas de segurança serão ignorados.

11.1. Flags úteis do Brakeman

Informar explicitamente o caminho da aplicação:

bundle exec brakeman -p .

O ponto representa o diretório atual.

Gerar um relatório JSON:

bundle exec brakeman -f json -o tmp/brakeman-report.json

O formato JSON facilita o consumo do relatório por outras ferramentas e processos de automação.

Consultar as opções disponíveis:

bundle exec brakeman --help

11.2. Como interpretar os alertas?

Um relatório pode apresentar uma suspeita de SQL Injection, XSS, atribuição de parâmetros ou outra categoria de risco.

Isso não significa que toda ocorrência seja necessariamente explorável. É preciso analisar o contexto, os dados que chegam ao trecho, as proteções existentes e o fluxo real da aplicação.

O processo correto é:

  1. identificar o arquivo e o trecho apontado;
  2. entender qual padrão o Brakeman reconheceu;
  3. verificar se a entrada pode ser controlada por alguém não confiável;
  4. avaliar as proteções e o contexto de execução;
  5. corrigir o problema quando confirmado;
  6. registrar e justificar adequadamente qualquer alerta considerado falso positivo.

Não devemos simplesmente desativar alertas para deixar o relatório limpo.

O Brakeman não substitui uma auditoria completa, testes de segurança, revisão de configuração, análise de infraestrutura ou avaliação de dependências.

Documentação: Brakeman — opções de linha de comando.

12. Bundler Audit: verificando vulnerabilidades nas gems

Aplicações Rails dependem de bibliotecas externas. Essas dependências podem conter vulnerabilidades conhecidas, mesmo quando o código desenvolvido pela equipe está correto.

O Bundler Audit verifica o arquivo de dependências resolvidas do projeto, normalmente Gemfile.lock, comparando as versões encontradas com uma base de avisos de segurança. Também verifica fontes de gems consideradas inseguras.

O comando habitual é:

bundle exec bundle-audit check

O executável fornecido pela gem é normalmente bundle-audit. Por isso, bundle exec bundler-audit check pode não encontrar o executável.

12.1. Atualizando a base de avisos

Para atualizar a base de avisos e executar a verificação:

bundle exec bundle-audit check --update

Atualizar a base de avisos não é a mesma coisa que atualizar as gems da aplicação.

12.2. Outras opções úteis

Exibir detalhes dos avisos:

bundle exec bundle-audit check --verbose

Executar sem atualizar a base de avisos:

bundle exec bundle-audit check --no-update

Essa opção pode ser útil em ambientes controlados, desde que a equipe saiba que a base local pode estar desatualizada.

Verificar um arquivo de lock específico:

bundle exec bundle-audit check --gemfile-lock Gemfile.lock

Consultar as opções disponíveis na versão instalada:

bundle exec bundle-audit check --help

Os parâmetros disponíveis podem variar conforme a versão. Antes de incorporá-los a scripts de CI/CD, confirme a ajuda local.

12.3. Encontrou uma vulnerabilidade. E agora?

Um aviso precisa ser investigado.

É necessário identificar a gem afetada, a versão utilizada, o problema descrito, as versões corrigidas e as possíveis restrições de compatibilidade.

Em seguida, podemos avaliar a atualização necessária, executar os testes e verificar se a correção resolveu o problema sem introduzir regressões.

Ignorar um alerta sem justificativa é diferente de avaliar o risco e documentar uma decisão técnica.

Documentação: RubySec — Bundler Audit.

13. Os comandos se complementam: não são concorrentes

Até aqui, cada ferramenta respondeu a uma pergunta diferente.

Ferramenta Pergunta principal
RSpec O comportamento esperado está sendo respeitado?
FactoryBot Como construir dados de teste de forma consistente?
Database Cleaner e transações Como impedir que um teste contamine outro?
SimpleCov Quais partes do código foram exercitadas pelos testes?
RuboCop O código viola regras estáticas e padrões definidos?
Brakeman Existem padrões de código que podem indicar vulnerabilidades Rails?
Bundler Audit As dependências possuem avisos de vulnerabilidades conhecidas?

Não faz sentido escolher apenas uma delas e acreditar que o restante está resolvido.

Um teste pode passar mesmo quando existe uma vulnerabilidade. O RuboCop pode aprovar um código cuja regra de negócio está errada. O Brakeman pode identificar um padrão suspeito, mas não confirmar sozinho toda a possibilidade de exploração. E o Bundler Audit pode apontar uma dependência vulnerável sem avaliar todo o contexto de utilização.

A qualidade vem da combinação dessas verificações, da interpretação dos resultados e das correções feitas pela equipe.

14. Uma rotina prática de validação para projetos Rails

Durante o desenvolvimento, podemos adotar um fluxo simples e adaptá-lo ao tamanho do projeto.

1. Executar os testes:

bundle exec rspec

2. Verificar a cobertura:

Inspecionar o relatório gerado pelo SimpleCov e investigar lacunas relevantes.

3. Executar o lint:

bundle exec rubocop

4. Executar a análise de segurança do Rails:

bundle exec brakeman -q

5. Auditar as dependências:

bundle exec bundle-audit check --update

6. Revisar as mudanças:

git diff

Esse fluxo é um ponto de partida, não uma sequência universal. Em projetos maiores, podemos dividir as etapas entre execução local, integração contínua e verificações periódicas.

Também é importante observar os códigos de saída dos comandos. Uma ferramenta que encontra problemas precisa conseguir sinalizá-los para que o pipeline não trate uma execução malsucedida como sucesso.

14.1. E a integração contínua?

Em um pipeline de CI, podemos executar essas verificações a cada pull request ou alteração relevante.

Uma política madura pode exigir que:

  • os testes sejam aprovados;
  • a cobertura não caia abaixo de um limite definido;
  • o RuboCop não encontre novas violações bloqueantes;
  • os alertas de segurança sejam avaliados conforme a política do projeto;
  • as dependências sejam verificadas regularmente.

Em bases de código antigas, pode ser mais viável estabelecer um patamar inicial e impedir que a situação piore, corrigindo o legado gradualmente.

A ideia não é bloquear o desenvolvimento por qualquer detalhe de formatação. É criar critérios consistentes e proporcionais ao risco.

15. Qualidade não se mede por um único número

Podemos ter uma aplicação com 100% de cobertura de linhas e ainda não testar adequadamente os resultados esperados.

Podemos ter um relatório do RuboCop sem violações e continuar com problemas arquiteturais.

Podemos executar o Brakeman sem alertas e ainda possuir vulnerabilidades que a ferramenta não detecta.

Podemos atualizar as gems e introduzir uma regressão funcional.

É justamente por isso que engenharia de software exige mais do que executar comandos.

Precisamos entender o que estamos verificando, interpretar os resultados e agir sobre eles.

Os testes automatizados ajudam a proteger comportamentos. A cobertura evidencia lacunas. O lint contribui para a consistência do código. A análise estática de segurança identifica padrões suspeitos. A auditoria de dependências ajuda a detectar versões associadas a vulnerabilidades conhecidas.

Nenhuma dessas práticas, isoladamente, oferece uma garantia absoluta. Juntas, porém, formam uma base importante para desenvolver e manter aplicações mais confiáveis.

E aqui está um ponto fundamental: essas ferramentas não eliminam a necessidade de conhecimento técnico. Pelo contrário, aumentam a importância de saber interpretar o que elas apontam.

A ferramenta pode identificar um problema. Mas alguém precisa compreender seu impacto, decidir como corrigi-lo e verificar se a solução realmente funciona.

Conclusão

Fala, Guerreiros e Guerreiras!

Testar não deveria ser uma etapa opcional, realizada apenas quando sobra tempo ou quando um cliente encontra um problema em produção.

Testes, análise estática e verificações de segurança precisam fazer parte do ciclo de desenvolvimento desde o início e continuar durante a manutenção da aplicação.

O objetivo não é colecionar ferramentas, atingir um percentual bonito ou produzir relatórios sem alertas. É reduzir riscos, detectar regressões mais cedo e aumentar a confiança nas mudanças que fazemos.

E isso vale tanto para o código escrito manualmente quanto para aquele produzido com auxílio de inteligência artificial: código gerado precisa ser compreendido, validado e mantido por quem assume a responsabilidade pelo software.

No próximo artigo, vamos avançar para outro ponto essencial: os alertas de segurança do GitHub, os CVEs, as dependências vulneráveis e a importância de manter aplicações e bibliotecas atualizadas.

Porque desenvolver software não termina quando o código funciona. Também precisamos cuidar do que colocamos em produção, das dependências que utilizamos e dos riscos que permanecem depois da entrega.

Texto escrito por humano, porém imagem gerada por IA.

Referências , Documentação e Recomendação