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).
- O que é um teste automatizado e por que vale a pena.
- Como usar o
assertdo<cassert>, e o que significamAssertion … failede o código 134. - Como montar um mini-framework de testes com poucas linhas.
- Como usar a biblioteca doctest:
TEST_CASE,CHECK,REQUIREedoctest::Approx. - Por que comparar
doublecom==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 olho | Teste automatizado |
|---|---|
| você digita valores e lê a saída | o programa de teste confere sozinho |
| confere o que lembrou de conferir | confere tudo o que está na lista, sempre |
| demora minutos | leva frações de segundo |
| uma mudança aqui quebra algo ali, e você só descobre depois | o 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(ocdo 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
assertque 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 oassertchama. 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) == 10falhou, mas não quanto a função devolveu. - Pode ser desligado. Compilando com
-DNDEBUG(de no debug, "sem depuração"), todos osassertsomem. 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.
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 oasserte as bibliotecas de teste conseguem mostrar a expressão. - quase(a, b)
- Compara dois
doublecom 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
makee octestolham 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
mainpor mim." O programa de testes não temmainseu: a doctest escreve um que roda todos os testes. Esta linha vem antes do#includee aparece em um só.cppdo 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 esperava10. No caso de R$ 200, devolveu20(10%), e o esperado era30(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 falhar | registra ERROR e continua o caso de teste | registra FATAL ERROR e encerra o caso de teste (os outros casos continuam) |
| Use quando | as verificações são independentes | o resto do teste não faz sentido (ou é perigoso) se esta falhar |
| Exemplo | CHECK(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 programatestes. 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
| Biblioteca | Como é | Exemplo de verificação |
|---|---|---|
| doctest | um arquivo só, compila rápido para o que oferece; a usada nesta aula | CHECK(a == b) |
| Catch2 | a "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 objetos | TEST(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.
- 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
| Escrita | O que faz |
|---|---|
#include <cassert> + assert(cond); | aborta o programa (código 134) se cond for falsa |
-DNDEBUG | desliga todos os assert |
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN | a 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 -s | mostra 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 build | roda os testes registrados |
Exercícios
1. O cabeçalho do assert
Complete o #include necessário para usar assert:
2. Comparar com tolerância
Complete o teste para aceitar o 19.990000000000002 que a conta devolve:
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 comAssertion `…' 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) eREQUIRE(encerra o caso), com os valores na falha e o placar no fim. Ela é um arquivo só, odoctest.h. - Nunca compare
doublecom==: usedoctest::Approx(ou uma função comoquase).19.99 == 19.99pode ser falso. - Teste as fronteiras: a maioria dos defeitos mora no
>que devia ser>=. - No CMake:
enable_testing(),add_test(…)ectest --test-dir build. Catch2 e GoogleTest funcionam com as mesmas ideias.