No artigo Redes de Segurança Automáticas — Sanitizers e clang-tidy ganhamos sanitizers e clang-tidy, e vimos que os sanitizers só pegam bugs nos caminhos que os testes efetivamente exercitam. Isso apontou diretamente para a peça que faltava na tríade da qualidade: os testes automatizados. Até aqui, você "testou" seus programas rodando-os e olhando a saída — um método frágil, manual e que não se repete. Testes automatizados são código que verifica, sozinho e a cada execução, que suas funções fazem o que devem. Mais que isso: eles pegam regressões — bugs reintroduzidos quando você muda algo que funcionava. Hoje aprendemos a escrever testes com o Catch2, um framework moderno e leve, e a integrá-los ao CMake. Ao terminar, você fecha a tríade — build organizado (Do Comando Único ao Projeto Real), detecção de erros (Redes de Segurança Automáticas) e verificação de correção (Provando que Funciona) — que sustenta todo software C++ profissional.
Por que testes automatizados
Suponha que você escreveu uma função soma e a testou imprimindo o resultado. Funciona. Meses depois, você "otimiza" algo próximo e, sem perceber, quebra a soma. Sem um teste automatizado, o bug passa despercebido até um usuário reclamar. Com um teste, ele é pego na hora, na sua máquina, antes de sair. Essa é a essência: testes são uma rede de segurança contra o futuro — contra as suas próprias mudanças posteriores. Eles também documentam o comportamento esperado (um teste diz "esta função, com estas entradas, deve dar este resultado") e dão coragem para refatorar, porque você sabe imediatamente se quebrou algo.
O Catch2 torna escrever testes quase trivial. Um teste é um bloco TEST_CASE com asserções REQUIRE que verificam condições:
// testes.cpp — usando Catch2 (versão 3.x)
#include <catch2/catch_test_macros.hpp>
// A função que queremos testar (normalmente viria de um cabeçalho do projeto).
int soma(int a, int b) { return a + b; }
TEST_CASE("soma de dois positivos") {
REQUIRE(soma(2, 3) == 5); // se falhar, o teste falha e reporta a linha
REQUIRE(soma(10, 20) == 30);
}
TEST_CASE("soma com zero e negativos") {
REQUIRE(soma(0, 0) == 0);
REQUIRE(soma(-5, 5) == 0);
REQUIRE(soma(-3, -4) == -7);
}
Cada TEST_CASE é um cenário nomeado; cada REQUIRE é uma verificação. Se soma(2, 3) não der 5, o Catch2 reporta exatamente qual asserção falhou, em qual linha, e quais foram os valores — muito mais útil que olhar uma saída à mão. E você não escreve main: o Catch2 fornece um, que roda todos os TEST_CASE e reporta um resumo (quantos passaram, quantos falharam).
O que testar: casos normais, extremos e de erro
Escrever testes bem é uma habilidade, e o princípio central é cobrir três tipos de caso. Os casos normais (o comportamento típico), os casos extremos (limites: zero, vazio, o maior valor possível, o primeiro e o último elemento) e os casos de erro (entradas inválidas, que devem falhar de forma controlada). Veja testando uma função mais interessante — a inversão de string do artigo O Fim do char — Textos de Verdade com std::string*:
#include <catch2/catch_test_macros.hpp>
#include <string>
std::string invertida(const std::string& s) {
std::string r;
for (std::size_t i = s.size(); i > 0; --i) r += s[i - 1];
return r;
}
TEST_CASE("inversão de string") {
// Caso normal:
REQUIRE(invertida("mundo") == "odnum");
// Casos extremos: string vazia e um único caractere.
REQUIRE(invertida("") == ""); // vazio: não deve quebrar
REQUIRE(invertida("a") == "a"); // um char: igual a si mesmo
// Caso interessante: palíndromo (invertido é igual ao original).
REQUIRE(invertida("arara") == "arara");
}
TEST_CASE("inversão é reversível") {
// Uma propriedade: inverter duas vezes volta ao original.
std::string original = "programacao";
REQUIRE(invertida(invertida(original)) == original);
}
Note o cuidado com os casos extremos: a string vazia e a de um caractere são exatamente onde bugs costumam se esconder (um laço que acessa s[i-1] com i=0 estouraria). Testá-los explicitamente garante robustez. O segundo TEST_CASE testa uma propriedade — "inverter duas vezes volta ao original" — uma forma poderosa de teste que não depende de você calcular o resultado esperado à mão.
Testando exceções e integrando ao CMake
O Catch2 também verifica que erros acontecem quando devem. Lembra da Pilha que lançava std::out_of_range ao desempilhar vazia (artigo Estruturas de Dados para Qualquer Tipo — Templates de Classe)? Você testa isso com REQUIRE_THROWS:
#include <catch2/catch_test_macros.hpp>
#include <stdexcept>
int raiz_inteira(int n) {
if (n < 0) throw std::invalid_argument("negativo");
int r = 0;
while ((r + 1) * (r + 1) <= n) ++r;
return r;
}
TEST_CASE("raiz inteira: casos válidos") {
REQUIRE(raiz_inteira(0) == 0);
REQUIRE(raiz_inteira(9) == 3);
REQUIRE(raiz_inteira(15) == 3); // piso da raiz
REQUIRE(raiz_inteira(16) == 4);
}
TEST_CASE("raiz inteira: entrada inválida lança") {
// Verifica que a exceção CERTA é lançada para entrada negativa.
REQUIRE_THROWS_AS(raiz_inteira(-1), std::invalid_argument);
}
REQUIRE_THROWS_AS verifica que a chamada lança a exceção do tipo esperado — testar o comportamento de erro é tão importante quanto testar o sucesso. Para rodar os testes, você os integra ao CMake como um executável ligado à biblioteca do Catch2 (obtida, por exemplo, via FetchContent, que o CMake baixa automaticamente):
# Trecho do CMakeLists.txt para testes
include(FetchContent)
FetchContent_Declare(Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.0)
FetchContent_MakeAvailable(Catch2)
add_executable(testes testes.cpp)
target_link_libraries(testes PRIVATE Catch2::Catch2WithMain) # Catch2 fornece o main
# Registra os testes para rodar com 'ctest'
enable_testing()
add_test(NAME todos_os_testes COMMAND testes)
Com isso, cmake --build build compila os testes e ctest --test-dir build os roda, reportando o que passou e o que falhou. Rodar os testes a cada mudança — idealmente de forma automática — é o que mantém a qualidade ao longo do tempo.
A honestidade sobre testes
Prometo a medida certa. Testes são essenciais, mas há equívocos a evitar. Primeiro, testar tudo cegamente é desperdício — priorize a lógica que importa (regras de negócio, algoritmos, casos extremos), não getters triviais. Segundo, testes dão confiança, não garantia: eles provam que os casos que você pensou funcionam, não que não há bugs — um caso que você não imaginou pode escapar. Terceiro, testes têm custo de manutenção: quando o comportamento muda legitimamente, os testes precisam mudar junto, e testes frágeis demais (que quebram a qualquer refatoração inofensiva) mais atrapalham que ajudam. A diretriz madura: escreva testes para o que tem risco de quebrar e importa se quebrar, cubra os casos extremos onde bugs se escondem, e trate a suíte de testes como código de primeira classe — mantida, legível, confiável. Testar bem é um investimento que se paga muitas vezes; testar mal é cerimônia.
Teste automatizado não prova ausência de bug; ele congela o comportamento que já funciona, para que uma mudança futura em outro lugar não o quebre em silêncio. É essa proteção contra regressão que "rodei e olhei a saída" não oferece, porque ninguém repete a conferência manual a cada alteração. O maior valor costuma estar nos casos extremos — vazio, um elemento, valor no limite — e no caminho de erro, que é justamente o menos exercitado em uso normal.
Fontes e leituras recomendadas
- github.com/catchorg/Catch2 (docs): a documentação oficial do Catch2, com
TEST_CASE,REQUIRE,SECTIONe as macros de exceção. - "Catch2 Tutorial" (na pasta docs do repositório): um guia passo a passo para começar, incluindo a integração com CMake via FetchContent.
- github.com/google/googletest: o GoogleTest, a alternativa mais usada em grandes projetos; vale conhecer as duas.
- Kent Beck, Test-Driven Development: By Example: o clássico sobre a filosofia de testes, aplicável a qualquer linguagem.
- ISO C++ Core Guidelines, seção sobre testes e a regra "test early and often": as diretrizes sobre a cultura de testes.
Exercícios
Exercício 1
Escreva uma função int maximo(const std::vector<int>& v) que devolva o maior elemento (assuma vetor não-vazio) e três TEST_CASE do Catch2: um caso normal, um com todos os elementos iguais, e um com um único elemento.
Ver resposta
✓ Resposta: Função e testes:
#include <catch2/catch_test_macros.hpp>
#include <vector>
#include <algorithm>
int maximo(const std::vector<int>& v) {
return *std::max_element(v.begin(), v.end());
}
TEST_CASE("máximo: caso normal") {
REQUIRE(maximo({3, 7, 2, 9, 1}) == 9);
}
TEST_CASE("máximo: todos iguais") {
REQUIRE(maximo({5, 5, 5}) == 5);
}
TEST_CASE("máximo: um elemento") {
REQUIRE(maximo({42}) == 42);
}
Exercício 2
Explique o conceito de regressão e como testes automatizados a previnem. Por que "testei rodando e olhando a saída" não protege contra regressões?
Ver resposta
✓ Resposta: Uma regressão é um bug reintroduzido num código que antes funcionava — normalmente causado por uma mudança feita em outro lugar que, sem querer, quebra um comportamento que já estava correto. Testes automatizados a previnem porque, ao rodá-los após cada mudança, qualquer comportamento que deixe de funcionar faz um teste falhar imediatamente, apontando o que quebrou. "Testar rodando e olhando a saída" não protege contra regressões porque é manual e não se repete: você olhou a saída uma vez, meses atrás, e não vai reexecutar e reinspecionar visualmente todos os casos a cada pequena mudança — é inviável lembrar de tudo e tedioso demais. O teste automatizado, ao contrário, reverifica todos os casos instantaneamente e sem esforço, sempre que você quiser, pegando a regressão no momento em que ela é introduzida.
Exercício 3
Para a função invertida da aula, proponha dois casos extremos além dos mostrados que valeria a pena testar, justificando por que cada um é um bom candidato a esconder bugs.
Ver resposta
✓ Resposta: Dois casos extremos adicionais valiosos para invertida: primeiro, uma string com caracteres repetidos e espaços (como "a b a"), que testa se o laço preserva corretamente todos os caracteres, incluindo espaços, na ordem certa — bugs de índice às vezes pulam ou duplicam elementos, e uma string com estrutura visível expõe isso. Segundo, uma string longa (dezenas ou centenas de caracteres), que testa se não há problema de desempenho grosseiro nem estouro em tamanhos maiores, e confirma que a lógica escala além dos casos minúsculos. Ambos são bons candidatos porque exercitam o laço em condições que os exemplos curtos ("mundo") não cobrem: o primeiro verifica preservação fiel de cada posição, o segundo verifica robustez em escala.
Exercício 4
Escreva um teste com REQUIRE_THROWS_AS para uma função dividir(int a, int b) que lança std::domain_error quando b == 0. Explique por que testar o comportamento de erro é tão importante quanto testar o sucesso.
Ver resposta
✓ Resposta: Teste de exceção:
#include <catch2/catch_test_macros.hpp>
#include <stdexcept>
int dividir(int a, int b) {
if (b == 0) throw std::domain_error("divisão por zero");
return a / b;
}
TEST_CASE("dividir: sucesso") {
REQUIRE(dividir(10, 2) == 5);
}
TEST_CASE("dividir: divisor zero lança domain_error") {
REQUIRE_THROWS_AS(dividir(1, 0), std::domain_error);
}
Testar o comportamento de erro é tão importante quanto testar o sucesso porque o tratamento de erros é parte do contrato da função: dividir promete lançar std::domain_error para b == 0, e código que depende dela conta com esse comportamento. Se, numa refatoração, alguém remover a checagem (fazendo a divisão por zero causar comportamento indefinido em vez da exceção), sem um teste isso passaria despercebido até causar um desastre. O REQUIRE_THROWS_AS verifica que a exceção certa é lançada, garantindo que o caminho de erro — frequentemente o menos exercitado no uso casual — permaneça correto.
Exercício 5
Discuta a afirmação "testes dão confiança, não garantia". Dê um exemplo de bug que uma suíte de testes razoável poderia deixar passar, e explique como você reduziria (sem eliminar) esse risco.
Ver resposta
✓ Resposta: A afirmação reconhece que uma suíte de testes prova que os cenários que você escreveu funcionam, mas não que não existam bugs — só testa o que foi imaginado. Exemplo de bug que poderia passar: uma função media que calcula corretamente para vetores normais (testados) mas sofre um overflow de inteiros ao somar muitos valores grandes — se nenhum teste usou valores grandes o suficiente para estourar, o bug escapa, mesmo com a suíte toda verde. Como reduzir (sem eliminar) esse risco: primeiro, cobrir deliberadamente casos extremos e limites (valores máximos, coleções enormes, entradas na fronteira do domínio), que é onde bugs se escondem; segundo, usar testes de propriedade que geram muitas entradas variadas em vez de exemplos fixos; terceiro, combinar os testes com os sanitizers do artigo Redes de Segurança Automáticas — Sanitizers e clang-tidy (rodar a suíte sob ASan/UBSan pegaria o overflow); e quarto, adicionar um teste específico assim que qualquer bug for descoberto em produção, para que aquela classe de erro nunca mais passe. Nenhuma dessas medidas dá garantia absoluta — sempre pode haver um caso não imaginado —, mas juntas elevam muito a confiança, que é o objetivo realista dos testes.