C++ Testes Automatizados

Toda vez que a Ana muda o programa da lojinha, ela roda, digita alguns valores e confere a resposta de olho. Funciona no começo. Mas o programa cresceu, e um dia ela mudou o cálculo do frete e quebrou, sem perceber, o cálculo do desconto, que ninguém tinha tocado. Um teste automatizado é um pequeno programa que confere o seu programa por você: em um segundo, ele roda dezenas de verificações e avisa exatamente qual falhou. Nesta aula você vai escrever testes de três jeitos, do mais simples (assert) ao usado em projetos de verdade (uma biblioteca de testes).

Você vai aprender
  • O que é um teste automatizado e por que vale a pena.
  • Como usar o assert do <cassert>, e o que significam Assertion … failed e o código 134.
  • Como montar um mini-framework de testes com poucas linhas.
  • Como usar a biblioteca doctest: TEST_CASE, CHECK, REQUIRE e doctest::Approx.
  • Por que comparar double com == engana até os testes.
  • Como rodar os testes pelo CMake (ctest), e o que são GoogleTest e Catch2.

Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp/testes, com o comando padrão do guia (-std=c++23 -Wall -Wextra). A doctest usada é a versão 2.4.12.


A ideia: a lista de conferência

Um piloto de avião não decola confiando na memória: ele segue uma lista de verificação (checklist), item por item, todas as vezes. Leva um minuto e pega o esquecimento que a pessoa mais experiente comete.

Um teste é um item dessa lista, escrito em C++: "o desconto de uma compra de R$ 100 tem que ser R$ 10". Você escreve uma vez e o computador confere sempre que quiser.

Testar de olhoTeste automatizado
você digita valores e lê a saídao programa de teste confere sozinho
confere o que lembrou de conferirconfere tudo o que está na lista, sempre
demora minutosleva frações de segundo
uma mudança aqui quebra algo ali, e você só descobre depoiso teste que falha aponta o que quebrou, na hora

A função que a Ana vai testar a aula inteira é a do desconto da lojinha. A regra é: compras a partir de R$ 100 ganham 10% de desconto; a partir de R$ 200, 15%. A função devolve o valor do desconto, em reais. Esta é a primeira versão dela; repare se você acha o defeito:

double calcularDesconto(double total) {
    if (total > 200) {
        return total * 0.15;
    }
    if (total > 100) {
        return total * 0.10;
    }
    return 0;
}

1. O primeiro teste: assert

A biblioteca padrão tem uma ferramenta pronta: assert ("afirmar"), do cabeçalho <cassert>. Você afirma algo que tem que ser verdade; se não for, o programa para na hora e diz onde.

#include <cassert>
#include <iostream>

// Desconto da lojinha: 10% a partir de R$ 100, 15% a partir de R$ 200
double calcularDesconto(double total) {
    if (total > 200) {
        return total * 0.15;
    }
    if (total > 100) {
        return total * 0.10;
    }
    return 0;
}

int main() {
    assert(calcularDesconto(50) == 0);
    assert(calcularDesconto(300) == 45);
    assert(calcularDesconto(100) == 10);
    std::cout << "Todos os testes passaram!\n";
}
#include <cassert>
O cabeçalho do assert (o c do começo indica que ele veio da linguagem C).
assert(calcularDesconto(50) == 0);
"Afirmo que o desconto de R$ 50 é zero." Se for verdade, nada acontece e o programa segue. Se for falso, o programa é interrompido.
calcularDesconto(100) == 10
O caso que mais interessa: o valor exato da fronteira, R$ 100. A regra diz "a partir de", então R$ 100 já tem desconto.
$ g++ -std=c++23 -Wall -Wextra assert1.cpp -o assert1
$ ./assert1
assert1: assert1.cpp:18: int main(): Assertion `calcularDesconto(100) == 10' failed.
Abortado                   (imagem do núcleo gravada) ./assert1
$ echo $?
134

O teste achou o defeito. Leia a mensagem por partes:

assert1:
O nome do programa.
assert1.cpp:18:
O arquivo e a linha do assert que falhou.
int main():
A função onde ele está.
Assertion `calcularDesconto(100) == 10' failed.
"A afirmação … falhou", com o texto exato que você escreveu.
Abortado (imagem do núcleo gravada)
Isso é o Bash avisando que o programa foi abortado, interrompido à força pela função abort(), que o assert chama. A "imagem do núcleo" (core dump) é uma fotografia da memória do programa para depuração; você pode ignorá-la.
134
O código de saída de um programa abortado: 128 + 6, onde 6 é o número do sinal SIGABRT ("sinal de abortar"). Qualquer código diferente de 0 quer dizer "deu errado".

O defeito: o código usa > ("maior que"), mas a regra diz "a partir de", que é >= ("maior ou igual"). Com R$ 100, 100 > 100 é falso, e o desconto ficou zero. É o tipo de erro que se esconde nas fronteiras, e por isso os bons testes sempre conferem o valor exato onde a regra muda. Trocando os dois > por >=:

$ g++ -std=c++23 -Wall -Wextra assert1.cpp -o assert1
$ ./assert1
Todos os testes passaram!
$ echo $?
0

Os limites do assert

  • Para no primeiro erro. Se três testes estão errados, você descobre um por vez.
  • Não mostra os valores. Diz que calcularDesconto(100) == 10 falhou, mas não quanto a função devolveu.
  • Pode ser desligado. Compilando com -DNDEBUG (de no debug, "sem depuração"), todos os assert somem. Com a versão defeituosa da função:
$ g++ -std=c++23 -Wall -Wextra -DNDEBUG assert1.cpp -o assert1
$ ./assert1
Todos os testes passaram!         « mentira: os assert foram apagados
$ echo $?
0

O assert é uma macro (veja O Pré-processador) que vira nada quando NDEBUG está definido. Muitos projetos compilam a versão final com -DNDEBUG, para não pagar o custo das verificações. Por isso, use o assert para conferir coisas "impossíveis" dentro do programa, e uma biblioteca de testes para testar.

Nunca ponha algo que o programa precisa dentro de um assert

assert(abrirArquivo()); funciona até o dia em que alguém compila com -DNDEBUG: a chamada some junto, e o arquivo nunca é aberto.


2. Um mini-framework de testes

Um framework de testes (em português, "arcabouço") é o código que roda os testes, conta quantos passaram e mostra os que falharam. Dá para fazer um, bem simples, em 15 linhas, e isso ajuda a entender o que as bibliotecas fazem por dentro:

#include <cmath>
#include <iostream>

// ---------- mini-framework de testes ----------
int testes = 0;
int falhas = 0;

void verificar(bool ok, const char* expressao, int linha) {
    ++testes;
    if (!ok) {
        ++falhas;
        std::cout << "linha " << linha << ": FALHOU: " << expressao << '\n';
    }
}

#define VERIFICAR(cond) verificar((cond), #cond, __LINE__)

bool quase(double a, double b) {
    return std::abs(a - b) < 0.005;   // diferença menor que meio centavo
}
// -----------------------------------------------

double calcularDesconto(double total) {
    if (total > 200) {
        return total * 0.15;
    }
    if (total > 100) {
        return total * 0.10;
    }
    return 0;
}

int main() {
    VERIFICAR(calcularDesconto(50) == 0);
    VERIFICAR(calcularDesconto(99.99) == 0);
    VERIFICAR(quase(calcularDesconto(100), 10));
    VERIFICAR(quase(calcularDesconto(150), 15));
    VERIFICAR(quase(calcularDesconto(200), 30));
    VERIFICAR(quase(calcularDesconto(300), 45));

    std::cout << testes - falhas << " de " << testes << " testes passaram\n";
    return falhas == 0 ? 0 : 1;
}
testes, falhas
Dois contadores: quantas verificações rodaram e quantas falharam.
verificar(ok, expressao, linha)
Conta a verificação e, se ela falhou, mostra a linha e o texto. Diferente do assert, não para: segue para a próxima.
#define VERIFICAR(cond) …
Uma macro, para não ter de escrever o texto e a linha à mão. O #cond (um # antes do parâmetro) transforma o que você escreveu em texto entre aspas; o __LINE__ vira o número da linha. É assim que o assert e as bibliotecas de teste conseguem mostrar a expressão.
quase(a, b)
Compara dois double com tolerância de meio centavo. O motivo aparece na seção 4.
return falhas == 0 ? 0 : 1;
O código de saída diz se tudo passou (0) ou não (1). Ferramentas como o make e o ctest olham esse código.
$ g++ -std=c++23 -Wall -Wextra mini.cpp -o mini
$ ./mini
linha 36: FALHOU: quase(calcularDesconto(100), 10)
linha 38: FALHOU: quase(calcularDesconto(200), 30)
4 de 6 testes passaram
$ echo $?
1

Agora as duas fronteiras aparecem de uma vez: R$ 100 e R$ 200.

Missão: rode e veja as duas falhas. Conserte só a função calcularDesconto para que a saída seja 6 de 6 testes passaram. Depois, acrescente um teste seu: por exemplo, que uma compra de R$ 250 tem R$ 37,50 de desconto.

#include <cmath>
#include <iostream>

// ---------- mini-framework de testes ----------
int testes = 0;
int falhas = 0;

void verificar(bool ok, const char* expressao, int linha) {
    ++testes;
    if (!ok) {
        ++falhas;
        std::cout << "linha " << linha << ": FALHOU: " << expressao << '\n';
    }
}

#define VERIFICAR(cond) verificar((cond), #cond, __LINE__)

bool quase(double a, double b) {
    return std::abs(a - b) < 0.005;   // diferença menor que meio centavo
}
// -----------------------------------------------

double calcularDesconto(double total) {
    if (total > 200) {
        return total * 0.15;
    }
    if (total > 100) {
        return total * 0.10;
    }
    return 0;
}

int main() {
    VERIFICAR(calcularDesconto(50) == 0);
    VERIFICAR(calcularDesconto(99.99) == 0);
    VERIFICAR(quase(calcularDesconto(100), 10));
    VERIFICAR(quase(calcularDesconto(150), 15));
    VERIFICAR(quase(calcularDesconto(200), 30));
    VERIFICAR(quase(calcularDesconto(300), 45));

    std::cout << testes - falhas << " de " << testes << " testes passaram\n";
    return falhas == 0 ? 0 : 1;
}
#include <cmath>
#include <iostream>

// ---------- mini-framework de testes ----------
int testes = 0;
int falhas = 0;

void verificar(bool ok, const char* expressao, int linha) {
    ++testes;
    if (!ok) {
        ++falhas;
        std::cout << "linha " << linha << ": FALHOU: " << expressao << '\n';
    }
}

#define VERIFICAR(cond) verificar((cond), #cond, __LINE__)

bool quase(double a, double b) {
    return std::abs(a - b) < 0.005;   // diferença menor que meio centavo
}
// -----------------------------------------------

double calcularDesconto(double total) {
    if (total >= 200) {
        return total * 0.15;
    }
    if (total >= 100) {
        return total * 0.10;
    }
    return 0;
}

int main() {
    VERIFICAR(calcularDesconto(50) == 0);
    VERIFICAR(calcularDesconto(99.99) == 0);
    VERIFICAR(quase(calcularDesconto(100), 10));
    VERIFICAR(quase(calcularDesconto(150), 15));
    VERIFICAR(quase(calcularDesconto(200), 30));
    VERIFICAR(quase(calcularDesconto(300), 45));

    std::cout << testes - falhas << " de " << testes << " testes passaram\n";
    return falhas == 0 ? 0 : 1;
}

3. Uma biblioteca de testes: doctest

O mini-framework funciona, mas falta muita coisa: agrupar testes por assunto, mostrar os valores que deram errado, escolher quais testes rodar… As bibliotecas de testes trazem tudo isso pronto. A doctest é uma das mais simples de começar, porque é header-only ("só cabeçalho"): a biblioteca inteira é um arquivo, doctest.h, que você põe na pasta do projeto e inclui. Não há nada para instalar nem para ligar (veja Usar Bibliotecas Externas).

Baixar o doctest.h

O arquivo fica no repositório oficial do projeto, no GitHub. A Ana baixou a versão 2.4.12 para a pasta do projeto com o wget (-q = sem mensagens):

$ cd ~/cpp/testes
$ wget -q https://raw.githubusercontent.com/doctest/doctest/v2.4.12/doctest/doctest.h
$ ls -l doctest.h
-rw-rw-r-- 1 ana ana 323200 out 16 22:02 doctest.h

São 316 KB de código C++, com a licença MIT (livre para usar, inclusive em projetos comerciais) escrita no começo do arquivo. Fixar a versão no endereço (v2.4.12) garante que todo mundo do projeto use a mesma.

Separar o código dos testes

Em projetos de verdade, os testes ficam num programa separado, que usa as mesmas funções do programa principal. Por isso a Ana tirou a função do main.cpp e criou um par .h/.cpp (como em Arquivos .h e .cpp):

#pragma once

// Desconto da lojinha: 10% a partir de R$ 100, 15% a partir de R$ 200.
// Devolve o valor do desconto, em reais.
double calcularDesconto(double total);
#include "desconto.h"

double calcularDesconto(double total) {
    if (total > 200) {
        return total * 0.15;
    }
    if (total > 100) {
        return total * 0.10;
    }
    return 0;
}

(Ela deixou o defeito de propósito, para ver a doctest pegá-lo.) E o arquivo de testes:

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "desconto.h"

TEST_CASE("compras abaixo de R$ 100 não têm desconto") {
    CHECK(calcularDesconto(0) == 0);
    CHECK(calcularDesconto(50) == 0);
    CHECK(calcularDesconto(99.99) == 0);
}

TEST_CASE("10% a partir de R$ 100") {
    CHECK(calcularDesconto(100) == 10);
    CHECK(calcularDesconto(150) == 15);
}

TEST_CASE("15% a partir de R$ 200") {
    CHECK(calcularDesconto(200) == 30);
    CHECK(calcularDesconto(300) == 45);
}
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
"Doctest, crie o main por mim." O programa de testes não tem main seu: a doctest escreve um que roda todos os testes. Esta linha vem antes do #include e aparece em um só .cpp do programa de testes.
#include "doctest.h"
Com aspas, porque o arquivo está na pasta do projeto.
TEST_CASE("…") { … }
Um caso de teste: um grupo de verificações sobre um assunto, com um nome que aparece no relatório. Escreva o nome como uma frase que diz o que deveria acontecer.
CHECK(expressão)
"Confira": se a expressão for falsa, registra a falha, com os valores, e continua.

Para compilar, junte o arquivo de testes com o .cpp da função testada:

$ time g++ -std=c++23 -Wall -Wextra testes.cpp desconto.cpp -o testes

real	0m4,105s
user	0m3,823s
sys	0m0,280s
$ ./testes
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
===============================================================================
testes.cpp:11:
TEST CASE:  10% a partir de R$ 100

testes.cpp:12: ERROR: CHECK( calcularDesconto(100) == 10 ) is NOT correct!
  values: CHECK( 0 == 10 )

===============================================================================
testes.cpp:16:
TEST CASE:  15% a partir de R$ 200

testes.cpp:17: ERROR: CHECK( calcularDesconto(200) == 30 ) is NOT correct!
  values: CHECK( 20 == 30 )

===============================================================================
[doctest] test cases: 3 | 1 passed | 2 failed | 0 skipped
[doctest] assertions: 7 | 5 passed | 2 failed |
[doctest] Status: FAILURE!
$ echo $?
1
testes.cpp:11: / TEST CASE: 10% a partir de R$ 100
Em qual caso de teste (e em que linha ele começa) houve falha.
testes.cpp:12: ERROR: CHECK( … ) is NOT correct!
A verificação da linha 12 "NÃO está correta".
values: CHECK( 0 == 10 )
A melhor parte: os valores dos dois lados. A função devolveu 0, e o teste esperava 10. No caso de R$ 200, devolveu 20 (10%), e o esperado era 30 (15%).
test cases: 3 | 1 passed | 2 failed
O placar: 3 casos de teste, 1 passou, 2 falharam. Na linha de baixo, o placar das verificações (assertions): 7, com 5 certas.
Status: FAILURE! e o código 1
Resultado geral e código de saída diferente de zero.

Os 4 segundos de compilação são quase todos da doctest, que é grande. Em projetos maiores, o DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN fica num .cpp só dele, compilado uma vez, e os arquivos de teste (que só fazem #include "doctest.h") ficam leves; o Make ou o CMake cuidam de não recompilá-lo. Com o >= no lugar do > no desconto.cpp:

$ g++ -std=c++23 -Wall -Wextra testes.cpp desconto.cpp -o testes
$ ./testes
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
===============================================================================
[doctest] test cases: 3 | 3 passed | 0 failed | 0 skipped
[doctest] assertions: 7 | 7 passed | 0 failed |
[doctest] Status: SUCCESS!
$ echo $?
0

Quando tudo passa, a doctest fica quieta e só mostra o placar. Para ver cada verificação, mesmo as que passaram, use -s (de success):

$ ./testes -s | head -20
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
===============================================================================
testes.cpp:5:
TEST CASE:  compras abaixo de R$ 100 não têm desconto

testes.cpp:6: SUCCESS: CHECK( calcularDesconto(0) == 0 ) is correct!
  values: CHECK( 0 == 0 )

testes.cpp:7: SUCCESS: CHECK( calcularDesconto(50) == 0 ) is correct!
  values: CHECK( 0 == 0 )

testes.cpp:8: SUCCESS: CHECK( calcularDesconto(99.99) == 0 ) is correct!
  values: CHECK( 0 == 0 )

===============================================================================
testes.cpp:11:
TEST CASE:  10% a partir de R$ 100

testes.cpp:12: SUCCESS: CHECK( calcularDesconto(100) == 10 ) is correct!

4. double e ==: o teste que mente

A Ana acrescentou um teste com centavos: uma compra de R$ 199,90 deve ter R$ 19,99 de desconto (10%). Num arquivo testes2.cpp, com o mesmo começo do testes.cpp:

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "desconto.h"

#include <vector>

TEST_CASE("centavos") {
    CHECK(calcularDesconto(199.90) == 19.99);
    CHECK(calcularDesconto(199.90) == doctest::Approx(19.99));
}

TEST_CASE("carrinho com três itens") {
    std::vector<double> carrinho = {59.90, 45.00};
    REQUIRE(carrinho.size() == 3);
    CHECK(carrinho[2] == 12.50);
}
$ g++ -std=c++23 -Wall -Wextra testes2.cpp desconto.cpp -o testes2
$ ./testes2
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
===============================================================================
testes2.cpp:7:
TEST CASE:  centavos

testes2.cpp:8: ERROR: CHECK( calcularDesconto(199.90) == 19.99 ) is NOT correct!
  values: CHECK( 19.99 == 19.99 )

===============================================================================
testes2.cpp:12:
TEST CASE:  carrinho com três itens

testes2.cpp:14: FATAL ERROR: REQUIRE( carrinho.size() == 3 ) is NOT correct!
  values: REQUIRE( 2 == 3 )

===============================================================================
[doctest] test cases: 2 | 0 passed | 2 failed | 0 skipped
[doctest] assertions: 3 | 1 passed | 2 failed |
[doctest] Status: FAILURE!
$ echo $?
1

Olhe a linha 8: values: CHECK( 19.99 == 19.99 ) e, mesmo assim, NOT correct. Não é defeito da doctest. Como você viu em float e double, o double guarda números em binário, e a maioria das frações decimais não cabe exatamente: 199.90 * 0.10 dá 19.990000000000002, e o literal 19.99 é outro número muito próximo. A doctest mostra os valores arredondados, e eles parecem iguais.

A linha 9 passou: doctest::Approx(19.99) quer dizer "aproximadamente 19,99", com uma pequena tolerância, como a função quase do mini-framework. Regra: nunca compare double com == num teste (nem fora dele); use doctest::Approx. (Em sistemas de verdade, dinheiro costuma ser guardado em centavos, num inteiro, justamente para evitar isso.)

CHECK × REQUIRE

No segundo caso, a Ana esqueceu um item do carrinho, que tem 2 itens em vez de 3. Repare que a falha diz FATAL ERROR e que o placar mostra 3 verificações, não 4: a linha 15 nem rodou.

CHECK(…)REQUIRE(…)
Se falharregistra ERROR e continua o caso de testeregistra FATAL ERROR e encerra o caso de teste (os outros casos continuam)
Use quandoas verificações são independenteso resto do teste não faz sentido (ou é perigoso) se esta falhar
ExemploCHECK(calcularDesconto(100) == 10);REQUIRE(carrinho.size() == 3); antes de usar carrinho[2]

Aqui, o REQUIRE evitou um desastre: sem ele, a linha 15 leria carrinho[2], uma posição que não existe, e isso é comportamento indefinido.

Rodar só alguns testes

Com muitos testes, dá para escolher pelo nome:

$ ./testes2 --test-case="centavos"
[doctest] doctest version is "2.4.12"
[doctest] run with "--help" for options
===============================================================================
testes2.cpp:7:
TEST CASE:  centavos

testes2.cpp:8: ERROR: CHECK( calcularDesconto(199.90) == 19.99 ) is NOT correct!
  values: CHECK( 19.99 == 19.99 )

===============================================================================
[doctest] test cases: 1 | 0 passed | 1 failed | 1 skipped
[doctest] assertions: 2 | 1 passed | 1 failed |
[doctest] Status: FAILURE!

5. Os testes no CMake: ctest

O CMake tem um comando para rodar todos os programas de teste do projeto, o ctest. A Ana pôs o desconto numa biblioteca interna (como na aula CMake) e registrou o teste:

cmake_minimum_required(VERSION 3.25)
project(Lojinha LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_library(desconto desconto.cpp)

enable_testing()
add_executable(testes testes.cpp)
target_link_libraries(testes PRIVATE desconto)
add_test(NAME testes COMMAND testes)
enable_testing()
Liga o suporte a testes do CMake.
add_test(NAME testes COMMAND testes)
Registra um teste chamado testes, que consiste em rodar o programa testes. Se ele sair com código 0, passou.
$ cmake -B build -G Ninja > /dev/null
$ cmake --build build
[1/4] Building CXX object CMakeFiles/desconto.dir/desconto.cpp.o
[2/4] Linking CXX static library libdesconto.a
[3/4] Building CXX object CMakeFiles/testes.dir/testes.cpp.o
[4/4] Linking CXX executable testes
$ ctest --test-dir build
Test project /home/ana/cpp/testes/build
    Start 1: testes
1/1 Test #1: testes ...........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.01 sec

E se alguém quebrar a fronteira de novo (voltando o >= para >)? Só o desconto.cpp é recompilado, e o ctest avisa:

$ cmake --build build
[1/3] Building CXX object CMakeFiles/desconto.dir/desconto.cpp.o
[2/3] Linking CXX static library libdesconto.a
[3/3] Linking CXX executable testes
$ ctest --test-dir build
Test project /home/ana/cpp/testes/build
    Start 1: testes
1/1 Test #1: testes ...........................***Failed    0.00 sec

0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.00 sec

The following tests FAILED:
	  1 - testes (Failed)
Errors while running CTest
Output from these tests are in: /home/ana/cpp/testes/build/Testing/Temporary/LastTest.log
Use "--rerun-failed --output-on-failure" to re-run the failed cases verbosely.
$ echo $?
8

O ctest só diz qual programa falhou. Para ver o relatório da doctest, siga a sugestão da última linha: ctest --test-dir build --rerun-failed --output-on-failure. Num Makefile, o equivalente é um alvo test (no .PHONY) cuja receita compila e roda o ./testes.


6. Outras bibliotecas de testes

BibliotecaComo éExemplo de verificação
doctestum arquivo só, compila rápido para o que oferece; a usada nesta aulaCHECK(a == b)
Catch2a "inspiração" da doctest, com o mesmo estilo de TEST_CASE; a versão 3 é uma biblioteca compilada (não mais um arquivo só)CHECK(a == b), REQUIRE(…)
GoogleTest (gtest)do Google, muito usada em empresas; biblioteca compilada, vem com o GoogleMock para "dublês" de objetosTEST(Desconto, Fronteira) { EXPECT_EQ(a, b); ASSERT_EQ(…); }

As ideias são as mesmas em todas: casos de teste com nome, verificações que continuam (CHECK, EXPECT_…) e que encerram (REQUIRE, ASSERT_…), relatório com os valores e código de saída para as ferramentas. Aprendeu uma, aprendeu todas. Como instalar bibliotecas compiladas é o assunto da próxima aula, Usar Bibliotecas Externas.

O que testar
  • As fronteiras: exatamente R$ 100 e R$ 200, e logo antes (R$ 99,99).
  • Os casos comuns: uma compra típica de cada faixa.
  • Os casos estranhos: zero, valores negativos, uma lista vazia, um texto vazio.
  • Cada defeito que você consertar: escreva um teste que o pegaria, para ele nunca voltar.

Há quem escreva o teste antes da função: primeiro o teste falha (porque a função não existe ou está errada), depois você escreve o código até ele passar. Isso se chama TDD (Test-Driven Development, "desenvolvimento guiado por testes"), e é o que você vai fazer na missão final.


Referência

EscritaO que faz
#include <cassert> + assert(cond);aborta o programa (código 134) se cond for falsa
-DNDEBUGdesliga todos os assert
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAINa doctest cria o main (em um só .cpp, antes do #include)
TEST_CASE("nome") { … }um caso de teste
CHECK(cond)verifica e continua
REQUIRE(cond)verifica e encerra o caso de teste se falhar
doctest::Approx(x)"aproximadamente x", para double
./testes -smostra também as verificações que passaram
./testes --test-case="nome"roda só os casos com esse nome
enable_testing() + add_test(…)registra testes no CMake
ctest --test-dir buildroda os testes registrados

Exercícios

1. O cabeçalho do assert

Complete o #include necessário para usar assert:

#include <>

2. Comparar com tolerância

Complete o teste para aceitar o 19.990000000000002 que a conta devolve:

CHECK(calcularDesconto(199.90) == );

3. Um programa terminou com Assertion `total > 0' failed. e echo $? mostrou 134. O que aconteceu?

4. Qual é a diferença entre CHECK e REQUIRE na doctest?

5. A regra é "frete grátis a partir de R$ 150". Qual teste tem mais chance de achar um > no lugar de >=?

Missão final: escreva a função a partir dos testes

Missão: desta vez os testes vieram primeiro (TDD). A regra do frete da lojinha é: compras a partir de R$ 150 têm frete grátis; abaixo disso, o frete é R$ 12,90, e para o bairro "Centro" é R$ 5,00. Escreva o corpo de calcularFrete até a saída ser 7 de 7 testes passaram. Leia os testes: eles são a especificação.

#include <cmath>
#include <iostream>
#include <string>

// ---------- mini-framework de testes ----------
int testes = 0;
int falhas = 0;

void verificar(bool ok, const char* expressao, int linha) {
    ++testes;
    if (!ok) {
        ++falhas;
        std::cout << "linha " << linha << ": FALHOU: " << expressao << '\n';
    }
}

#define VERIFICAR(cond) verificar((cond), #cond, __LINE__)

bool quase(double a, double b) {
    return std::abs(a - b) < 0.005;
}
// -----------------------------------------------

double calcularFrete(double total, const std::string& bairro) {
    // escreva aqui
    (void)total;
    (void)bairro;
    return -1;
}

int main() {
    VERIFICAR(quase(calcularFrete(50, "Jardim"), 12.90));
    VERIFICAR(quase(calcularFrete(149.99, "Jardim"), 12.90));
    VERIFICAR(quase(calcularFrete(150, "Jardim"), 0));
    VERIFICAR(quase(calcularFrete(400, "Jardim"), 0));
    VERIFICAR(quase(calcularFrete(50, "Centro"), 5));
    VERIFICAR(quase(calcularFrete(149.99, "Centro"), 5));
    VERIFICAR(quase(calcularFrete(150, "Centro"), 0));

    std::cout << testes - falhas << " de " << testes << " testes passaram\n";
    return falhas == 0 ? 0 : 1;
}
#include <cmath>
#include <iostream>
#include <string>

// ---------- mini-framework de testes ----------
int testes = 0;
int falhas = 0;

void verificar(bool ok, const char* expressao, int linha) {
    ++testes;
    if (!ok) {
        ++falhas;
        std::cout << "linha " << linha << ": FALHOU: " << expressao << '\n';
    }
}

#define VERIFICAR(cond) verificar((cond), #cond, __LINE__)

bool quase(double a, double b) {
    return std::abs(a - b) < 0.005;
}
// -----------------------------------------------

double calcularFrete(double total, const std::string& bairro) {
    if (total >= 150) {
        return 0;
    }
    if (bairro == "Centro") {
        return 5;
    }
    return 12.90;
}

int main() {
    VERIFICAR(quase(calcularFrete(50, "Jardim"), 12.90));
    VERIFICAR(quase(calcularFrete(149.99, "Jardim"), 12.90));
    VERIFICAR(quase(calcularFrete(150, "Jardim"), 0));
    VERIFICAR(quase(calcularFrete(400, "Jardim"), 0));
    VERIFICAR(quase(calcularFrete(50, "Centro"), 5));
    VERIFICAR(quase(calcularFrete(149.99, "Centro"), 5));
    VERIFICAR(quase(calcularFrete(150, "Centro"), 0));

    std::cout << testes - falhas << " de " << testes << " testes passaram\n";
    return falhas == 0 ? 0 : 1;
}

Resumo

  • Um teste automatizado é um programa que confere o seu: roda sempre igual, em frações de segundo, e aponta o que quebrou.
  • assert(cond) (de <cassert>) aborta o programa com Assertion `…' failed. e código 134. Para no primeiro erro, não mostra valores e some com -DNDEBUG: bom para conferências internas, fraco para testar.
  • Bibliotecas de teste organizam tudo: na doctest, TEST_CASE("nome"), CHECK (continua) e REQUIRE (encerra o caso), com os valores na falha e o placar no fim. Ela é um arquivo só, o doctest.h.
  • Nunca compare double com ==: use doctest::Approx (ou uma função como quase). 19.99 == 19.99 pode ser falso.
  • Teste as fronteiras: a maioria dos defeitos mora no > que devia ser >=.
  • No CMake: enable_testing(), add_test(…) e ctest --test-dir build. Catch2 e GoogleTest funcionam com as mesmas ideias.