C Testes (assert)

Como você sabe que o seu programa funciona? Até agora, rodando e olhando a saída. Isso funciona uma vez; mas, a cada mudança no código, você precisaria rodar e conferir tudo de novo, à mão, e um dia vai esquecer de conferir justo a parte que quebrou. Nesta aula você vai aprender duas ferramentas: o assert, que faz o programa conferir sozinho as coisas que "têm de ser verdade", e os testes automáticos: um programa que chama as suas funções com valores conhecidos e confere as respostas, sempre que você quiser, em um segundo.

Você vai aprender
  • O que faz o assert (da assert.h) e como ler a mensagem Assertion … failed.
  • O que é NDEBUG e por que nunca colocar algo "que precisa acontecer" dentro do assert.
  • A escrever testes automáticos com uma macro TESTE(condição) que conta acertos e falhas.
  • Por que funções puras são fáceis de testar.
  • A rodar os testes com o CTest (do CMake) e os nomes dos frameworks de teste para C: Unity, CMocka e Criterion.

As saídas de terminal desta aula são reais, do gcc 15 na máquina fictícia da Ana (pasta ~/c/testes). O assert também funciona no editor do guia, mas a mensagem tem outras palavras (seção 1).


A ideia: o fiscal de qualidade

Numa fábrica de bolos, há duas conferências diferentes:

  • Durante o preparo, o confeiteiro tem regras que nunca podem ser quebradas: "o forno tem de estar ligado antes de pôr a massa". Se estiver desligado, algo está muito errado: ele para tudo e chama alguém. Isso é o assert: uma certeza escrita no código, conferida enquanto o programa roda.
  • Antes de vender um bolo novo, o fiscal prova amostras com uma lista: "o de chocolate tem gosto de chocolate? o de 1 kg pesa 1 kg?". Isso é o teste: um programa separado, com exemplos cuja resposta você já sabe.

1. assert: "isto tem de ser verdade"

A lojinha da Ana tem uma função que retira produtos do estoque. Ela só faz sentido com uma quantidade positiva e que exista no estoque:

#include <assert.h>
#include <stdio.h>

int retirar(int estoque, int quantidade) {
    assert(quantidade > 0);
    assert(quantidade <= estoque);
    return estoque - quantidade;
}

int main(void) {
    int estoque = 10;
    estoque = retirar(estoque, 3);
    printf("Sobrou: %d\n", estoque);
    estoque = retirar(estoque, 9);
    printf("Sobrou: %d\n", estoque);
    return 0;
}
#include <assert.h>
O assert mora no cabeçalho assert.h. Assert quer dizer "afirmar": você afirma que algo é verdade.
assert(quantidade > 0);
Se a condição for verdadeira, não acontece nada e o programa segue. Se for falsa, o programa mostra uma mensagem com a condição, o arquivo e a linha, e é encerrado na hora com abort().
assert(quantidade <= estoque);
A segunda certeza: não dá para retirar mais do que existe.
$ gcc -std=c23 -Wall -Wextra estoque.c -o estoque
$ ./estoque
Sobrou: 7
estoque: estoque.c:6: retirar: Assertion `quantidade <= estoque' failed.
Abortado                   (imagem do núcleo gravada) ./estoque
$ echo $?
134
Sobrou: 7
A primeira retirada (3 de 10) passou nos dois assert.
estoque: estoque.c:6: retirar:
O nome do programa, o arquivo e a linha do assert que falhou e a função onde ele está.
Assertion `quantidade <= estoque' failed.
"A afirmação quantidade <= estoque falhou": a condição, copiada do código, entre uma crase e um apóstrofo. Tentaram retirar 9 de um estoque de 7.
Abortado (imagem do núcleo gravada)
O assert chamou abort(), que mata o programa (veja Encerrar o Programa). O segundo "Sobrou" nunca apareceu.
134
O código de saída de um programa abortado (128 + 6, o número do sinal SIGABRT; veja Sinais).

No editor do guia, a biblioteca do C é outra (a musl, não a glibc), e a mensagem tem outro formato, com as mesmas informações:

Sobrou: 7
Assertion failed: quantidade <= estoque (estoque.c: retirar: 6)
Abortado (imagem do núcleo gravada)
[código de saída: 134]

Missão: este programa roda sem erro. Acrescente, antes do return 0;, uma chamada a retirar que faça falhar o primeiro assert (o quantidade > 0). Leia a mensagem e repare no código de saída 134.

#include <assert.h>
#include <stdio.h>

int retirar(int estoque, int quantidade) {
    assert(quantidade > 0);
    assert(quantidade <= estoque);
    return estoque - quantidade;
}

int main(void) {
    int estoque = 10;
    estoque = retirar(estoque, 3);
    printf("Sobrou: %d\n", estoque);
    estoque = retirar(estoque, 5);
    printf("Sobrou: %d\n", estoque);
    return 0;
}
#include <assert.h>
#include <stdio.h>

int retirar(int estoque, int quantidade) {
    assert(quantidade > 0);
    assert(quantidade <= estoque);
    return estoque - quantidade;
}

int main(void) {
    int estoque = 10;
    estoque = retirar(estoque, 3);
    printf("Sobrou: %d\n", estoque);
    estoque = retirar(estoque, 5);
    printf("Sobrou: %d\n", estoque);
    estoque = retirar(estoque, 0);
    return 0;
}

Uma mensagem junto: && "texto"

O assert mostra a condição como ela está escrita. Um truque comum para deixar a mensagem mais clara: acrescentar && "explicação". Uma string literal é sempre "verdadeira" (é um ponteiro que não é nulo), então ela não muda o resultado, mas aparece na mensagem:

#include <assert.h>
#include <stdio.h>

int main(void) {
    int alunos = 0;
    assert(alunos > 0 && "a turma não pode estar vazia");
    printf("Média por aluno: %d\n", 30 / alunos);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra msg.c -o msg
$ ./msg
msg: msg.c:6: main: Assertion `alunos > 0 && "a turma não pode estar vazia"' failed.
Abortado                   (imagem do núcleo gravada) ./msg

Sem o assert, o programa chegaria à divisão por zero (veja Comportamento Indefinido). Com ele, para antes, explicando o porquê.


2. Para que serve (e para que não serve) o assert

O assert é para erros do programador: coisas que, se o código estiver certo, nunca acontecem. Exemplos:

  • Um parâmetro que tem de vir num certo intervalo: assert(mes >= 1 && mes <= 12);
  • Um ponteiro que nunca pode ser nulo: assert(lista != NULL); (veja Ponteiro Nulo).
  • Um resultado que tem de fazer sentido: assert(total >= 0);

Ele não é para erros que podem acontecer de verdade, com o programa certo: o usuário digitar letras em vez de número, um arquivo que não existe, o malloc devolver NULL. Esses casos você trata com if, mostrando uma mensagem e seguindo (ou saindo com um código de erro); veja Validar a Entrada e errno. Um motivo forte é o da próxima seção: o assert pode ser desligado.


3. NDEBUG: desligando os assert

Se o nome NDEBUG (no debug, "sem depuração") estiver definido quando o assert.h é incluído, todo assert vira nada: o compilador nem gera o teste. A opção -D do gcc define um nome (veja Compilação Condicional):

$ gcc -std=c23 -Wall -Wextra -DNDEBUG estoque.c -o estoque
$ ./estoque
Sobrou: 7
Sobrou: -2
$ echo $?
0

Sem os assert, o erro passou: o estoque ficou negativo e o programa terminou "com sucesso". É por isso que o assert não serve para validar dados do mundo real. O NDEBUG existe para versões finais em que cada nanossegundo conta: o modo Release do CMake, por exemplo, acrescenta -DNDEBUG sozinho (veja CMake). Também dá para definir no próprio código, antes do include:

#define NDEBUG
#include <assert.h>
Nunca coloque dentro do assert algo que precisa acontecer

Com NDEBUG, a linha inteira some, inclusive o que estava dentro dos parênteses. Este programa lê a idade dentro do assert:

#include <assert.h>
#include <stdio.h>

int main(void) {
    int idade = 0;
    printf("Idade: ");
    assert(scanf("%d", &idade) == 1);
    printf("Ano que vem: %d\n", idade + 1);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra idade.c -o idade
$ echo 29 | ./idade
Idade: Ano que vem: 30
$ gcc -std=c23 -Wall -Wextra -DNDEBUG idade.c -o idade
$ echo 29 | ./idade
Idade: Ano que vem: 1

Na versão com -DNDEBUG, o scanf sumiu junto com o assert: a idade nunca foi lida e ficou 0. O certo é ler fora e conferir com if: if (scanf("%d", &idade) != 1) { … }.

Para conferências que podem ser feitas na compilação (por exemplo, "um int tem pelo menos 4 bytes"), existe o static_assert, que não custa nada quando o programa roda e não é desligado pelo NDEBUG (veja static_assert).


4. Testes automáticos

A Ana escreveu duas funções de datas, num arquivo separado do resto do programa:

#ifndef DATAS_H
#define DATAS_H

bool eh_bissexto(int ano);
int dias_no_mes(int mes, int ano);

#endif
#include "datas.h"

bool eh_bissexto(int ano) {
    return ano % 4 == 0;
}

int dias_no_mes(int mes, int ano) {
    switch (mes) {
    case 2:
        return eh_bissexto(ano) ? 29 : 28;
    case 4: case 6: case 9: case 11:
        return 30;
    default:
        return 31;
    }
}

(O bool não precisa de include porque, no C23, ele é palavra-chave; veja Booleanos.) Um teste é um outro programa, com um main próprio, que chama essas funções com exemplos cuja resposta a gente sabe de cor. A regra do ano bissexto: divisível por 4, exceto os divisíveis por 100, a não ser que sejam divisíveis por 400. Então 2024 é, 2023 não é, 1900 não é e 2000 é.

Primeira tentativa: uma lista de assert

#include <assert.h>
#include <stdio.h>
#include "datas.h"

int main(void) {
    assert(eh_bissexto(2024));
    assert(!eh_bissexto(2023));
    assert(!eh_bissexto(1900));
    assert(eh_bissexto(2000));
    printf("Todos os testes passaram.\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra datas.c teste_assert.c -o teste_assert
$ ./teste_assert
teste_assert: teste_assert.c:8: main: Assertion `!eh_bissexto(1900)' failed.
Abortado                   (imagem do núcleo gravada) ./teste_assert
$ echo $?
134

Achou um erro: 1900 não deveria ser bissexto. Mas o primeiro assert que falha para tudo: você não sabe se o 2000 passaria, nem quantos testes falham ao todo. Para testes, é melhor algo que confira todos e conte no fim.

Uma macro TESTE que conta

#include <stdio.h>
#include "datas.h"

static int testes = 0;
static int falhas = 0;

#define TESTE(cond) do {                                          \
        testes++;                                                 \
        if (cond) {                                               \
            printf("ok     %s\n", #cond);                         \
        } else {                                                  \
            falhas++;                                             \
            printf("FALHOU %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
        }                                                         \
    } while (0)

int main(void) {
    TESTE(eh_bissexto(2024));
    TESTE(!eh_bissexto(2023));
    TESTE(!eh_bissexto(1900));
    TESTE(eh_bissexto(2000));
    TESTE(dias_no_mes(1, 2025) == 31);
    TESTE(dias_no_mes(4, 2025) == 30);
    TESTE(dias_no_mes(2, 2024) == 29);
    TESTE(dias_no_mes(2, 1900) == 28);

    printf("%d de %d testes passaram\n", testes - falhas, testes);
    return falhas == 0 ? 0 : 1;
}
static int testes = 0; static int falhas = 0;
Dois contadores, visíveis só neste arquivo (veja static e extern).
#define TESTE(cond) …
Uma macro com parâmetro: cada TESTE(x) no código é trocado pelo bloco inteiro, com x no lugar de cond. A \ no fim de cada linha continua a macro na linha seguinte.
do { … } while (0)
Um laço que roda uma vez só. É o jeito tradicional de fazer uma macro de várias linhas se comportar como um comando único, que pede ; no fim e funciona dentro de um if sem chaves.
#cond
O # antes do parâmetro transforma o texto do argumento numa string: TESTE(eh_bissexto(2024)) imprime "eh_bissexto(2024)". É assim que o assert mostra a condição.
__FILE__ e __LINE__
Nomes que o pré-processador troca pelo nome do arquivo e pelo número da linha onde aparecem (veja Pré-processador). Como a macro é expandida na linha do TESTE, a linha é a do teste.
return falhas == 0 ? 0 : 1;
O código de saída diz se tudo passou: 0 = sucesso, 1 = alguma falha. Ferramentas como o make, o CTest e os servidores de integração contínua olham esse código.
$ gcc -std=c23 -Wall -Wextra datas.c teste_datas.c -o teste_datas
$ ./teste_datas
ok     eh_bissexto(2024)
ok     !eh_bissexto(2023)
FALHOU !eh_bissexto(1900) (teste_datas.c:20)
ok     eh_bissexto(2000)
ok     dias_no_mes(1, 2025) == 31
ok     dias_no_mes(4, 2025) == 30
ok     dias_no_mes(2, 2024) == 29
FALHOU dias_no_mes(2, 1900) == 28 (teste_datas.c:25)
6 de 8 testes passaram
$ echo $?
1

Agora o relatório é completo: duas falhas, e as duas apontam para a mesma causa (1900). O eh_bissexto(2000) passou por acaso: a regra "divisível por 4" acerta 2000, mas pelo motivo errado. Depois de consertar eh_bissexto com a regra inteira, a Ana roda os mesmos testes:

$ gcc -std=c23 -Wall -Wextra datas.c teste_datas.c -o teste_datas
$ ./teste_datas
ok     eh_bissexto(2024)
ok     !eh_bissexto(2023)
ok     !eh_bissexto(1900)
ok     eh_bissexto(2000)
ok     dias_no_mes(1, 2025) == 31
ok     dias_no_mes(4, 2025) == 30
ok     dias_no_mes(2, 2024) == 29
ok     dias_no_mes(2, 1900) == 28
8 de 8 testes passaram
$ echo $?
0

Daqui em diante, toda vez que mexer em datas.c, ela roda ./teste_datas e sabe em um segundo se quebrou alguma coisa. Teste que já pegou um erro fica para sempre: ele garante que o erro não volta.

Missão: conserte eh_bissexto (aba datas.c) com a regra completa: divisível por 4 e não por 100, ou divisível por 400 (veja Operadores Lógicos). Não mexa nos testes: a missão termina quando aparecer 8 de 8 testes passaram.

#include <stdio.h>
#include "datas.h"

static int testes = 0;
static int falhas = 0;

#define TESTE(cond) do {                                          \
        testes++;                                                 \
        if (cond) {                                               \
            printf("ok     %s\n", #cond);                         \
        } else {                                                  \
            falhas++;                                             \
            printf("FALHOU %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
        }                                                         \
    } while (0)

int main(void) {
    TESTE(eh_bissexto(2024));
    TESTE(!eh_bissexto(2023));
    TESTE(!eh_bissexto(1900));
    TESTE(eh_bissexto(2000));
    TESTE(dias_no_mes(1, 2025) == 31);
    TESTE(dias_no_mes(4, 2025) == 30);
    TESTE(dias_no_mes(2, 2024) == 29);
    TESTE(dias_no_mes(2, 1900) == 28);

    printf("%d de %d testes passaram\n", testes - falhas, testes);
    return falhas == 0 ? 0 : 1;
}
#include "datas.h"

bool eh_bissexto(int ano) {
    return ano % 4 == 0;
}

int dias_no_mes(int mes, int ano) {
    switch (mes) {
    case 2:
        return eh_bissexto(ano) ? 29 : 28;
    case 4: case 6: case 9: case 11:
        return 30;
    default:
        return 31;
    }
}
#ifndef DATAS_H
#define DATAS_H

bool eh_bissexto(int ano);
int dias_no_mes(int mes, int ano);

#endif
#include "datas.h"

bool eh_bissexto(int ano) {
    return (ano % 4 == 0 && ano % 100 != 0) || ano % 400 == 0;
}

int dias_no_mes(int mes, int ano) {
    switch (mes) {
    case 2:
        return eh_bissexto(ano) ? 29 : 28;
    case 4: case 6: case 9: case 11:
        return 30;
    default:
        return 31;
    }
}

5. Funções puras são fáceis de testar

Repare que eh_bissexto e dias_no_mes são fáceis de testar porque são funções puras: recebem tudo pelos parâmetros, devolvem o resultado com return e não fazem mais nada (não leem o teclado, não imprimem, não mexem em variáveis globais). A mesma entrada dá sempre a mesma saída.

Compare com esta função, que mistura conta e conversa com o usuário:

void mostrar_media(void) {
    double a, b;
    scanf("%lf %lf", &a, &b);
    printf("Média: %.1f\n", (a + b) / 2);
}

Para testá-la, seria preciso digitar números e ler a tela. Separe a conta (pura, testável) da entrada e saída:

double media(double a, double b) {
    return (a + b) / 2;
}

/* no main: lê com scanf, chama media(a, b), mostra com printf */

Agora TESTE(media(8, 10) == 9) confere a conta sem teclado nenhum. (Cuidado ao comparar double com ==: contas como 0.1 + 0.2 não dão exatamente 0.3. Compare com uma tolerância, fabs(x - esperado) < 1e-9; veja float e double.)

O que testar

  • Um caso comum: contar_vogais("banana") dá 3.
  • As bordas: a string vazia, o primeiro e o último mês, o zero, o maior valor.
  • Os casos especiais da regra: 1900 e 2000 no bissexto.
  • Todo erro que você já encontrou: escreva o teste que o pega antes de consertar.

Missão: agora é você quem escreve os testes. A função contar_vogais conta as vogais minúsculas sem acento de uma string. Já há um teste; acrescente pelo menos mais dois TESTE(contar_vogais(…) == …): por exemplo, a string vazia "" e uma palavra sem vogais, como "psst". Todos têm de passar.

#include <stdio.h>

static int testes = 0;
static int falhas = 0;

#define TESTE(cond) do {                                          \
        testes++;                                                 \
        if (cond) {                                               \
            printf("ok     %s\n", #cond);                         \
        } else {                                                  \
            falhas++;                                             \
            printf("FALHOU %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
        }                                                         \
    } while (0)

int contar_vogais(const char *s) {
    int n = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        char c = s[i];
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            n++;
        }
    }
    return n;
}

int main(void) {
    TESTE(contar_vogais("banana") == 3);

    printf("%d de %d testes passaram\n", testes - falhas, testes);
    return falhas == 0 ? 0 : 1;
}
#include <stdio.h>

static int testes = 0;
static int falhas = 0;

#define TESTE(cond) do {                                          \
        testes++;                                                 \
        if (cond) {                                               \
            printf("ok     %s\n", #cond);                         \
        } else {                                                  \
            falhas++;                                             \
            printf("FALHOU %s (%s:%d)\n", #cond, __FILE__, __LINE__); \
        }                                                         \
    } while (0)

int contar_vogais(const char *s) {
    int n = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        char c = s[i];
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            n++;
        }
    }
    return n;
}

int main(void) {
    TESTE(contar_vogais("banana") == 3);
    TESTE(contar_vogais("") == 0);
    TESTE(contar_vogais("psst") == 0);

    printf("%d de %d testes passaram\n", testes - falhas, testes);
    return falhas == 0 ? 0 : 1;
}

6. Rodar os testes com o CTest

Num projeto com CMake, o programa de teste vira mais um alvo, e o CTest (que vem junto com o CMake) roda todos os testes e resume o resultado. Bastam duas linhas no fim do CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(datas C)

set(CMAKE_C_STANDARD 23)

add_executable(teste_datas teste_datas.c datas.c)
target_compile_options(teste_datas PRIVATE -Wall -Wextra)

enable_testing()
add_test(NAME datas COMMAND teste_datas)
enable_testing()
"Ligue os testes" neste projeto.
add_test(NAME datas COMMAND teste_datas)
Um teste chamado datas, que roda o programa teste_datas. O CTest considera que passou se o código de saída for 0. Por isso o return falhas == 0 ? 0 : 1;.
$ cmake -B build > /dev/null
$ cmake --build build > /dev/null
$ ctest --test-dir build --output-on-failure
Test project /home/ana/c/testes/build
    Start 1: datas
1/1 Test #1: datas ............................***Failed    0.00 sec
ok     eh_bissexto(2024)
ok     !eh_bissexto(2023)
FALHOU !eh_bissexto(1900) (/home/ana/c/testes/teste_datas.c:20)
ok     eh_bissexto(2000)
ok     dias_no_mes(1, 2025) == 31
ok     dias_no_mes(4, 2025) == 30
ok     dias_no_mes(2, 2024) == 29
FALHOU dias_no_mes(2, 1900) == 28 (/home/ana/c/testes/teste_datas.c:25)
6 de 8 testes passaram


0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.00 sec

The following tests FAILED:
	  1 - datas (Failed)
Errors while running CTest

Com a versão errada de datas.c, o CTest marca ***Failed ("falhou") e, por causa do --output-on-failure ("mostre a saída quando falhar"), mostra o que o teste imprimiu. Repare que o __FILE__ agora tem o caminho completo: o CMake passa ao gcc os arquivos com o caminho inteiro. Com a versão consertada:

$ ctest --test-dir build
Test project /home/ana/c/testes/ok/build
    Start 1: datas
1/1 Test #1: datas ............................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.00 sec

7. Frameworks de teste para C

A macro TESTE é um framework de teste em miniatura. Em projetos maiores, use um pronto: eles separam os testes em grupos, mostram o valor esperado e o obtido (esperava 28, veio 29), rodam cada teste isolado e geram relatórios para servidores de integração contínua.

FrameworkComo éBom para
UnityUm .c e dois .h que você copia para o projeto; macros como TEST_ASSERT_EQUAL_INT(28, dias_no_mes(2, 1900)).Começar; sistemas embarcados (microcontroladores), onde é muito usado.
CMockaUma biblioteca (sudo apt install libcmocka-dev); tem também "dublês" (mocks) para fingir funções que você não quer chamar de verdade no teste.Testar código que fala com arquivos, rede ou hardware.
CriterionUma biblioteca moderna; cada teste é um bloco Test(grupo, nome) { … }, sem precisar de main; roda cada teste num processo separado, então um crash não derruba os outros.Projetos para Linux e macOS.
CTestNão é um framework, é um executor: roda os seus programas de teste (feitos com qualquer um dos acima, ou com a macro TESTE).Juntar todos os testes de um projeto CMake.

Qual usar: para aprender e para projetos pequenos, a macro TESTE desta aula basta. Quando o projeto crescer, Unity é o mais simples de adotar (é só copiar arquivos); Criterion é o mais confortável no Linux. Rode os testes também com os sanitizers ligados: um teste que passa mas lê memória proibida ainda é um erro.


Quando dá errado

Esqueceu o #include <assert.h>
$ cat semassert.c
#include <stdio.h>

int main(void) {
    int estoque = 0;
    assert(estoque > 0);
    printf("Estoque: %d\n", estoque);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra semassert.c -o semassert
semassert.c: In function ‘main’:
semassert.c:5:5: error: implicit declaration of function ‘assert’ [-Wimplicit-function-declaration]
    5 |     assert(estoque > 0);
      |     ^~~~~~
semassert.c:2:1: note: ‘assert’ is defined in header ‘<assert.h>’; this is probably fixable by adding ‘#include <assert.h>’
    1 | #include <stdio.h>
  +++ |+#include <assert.h>
    2 | 

O que aconteceu: "declaração implícita da função assert": o gcc não sabe o que é assert. A nota até diz a solução: "assert está definido em <assert.h>; provavelmente resolve acrescentar #include <assert.h>". No editor (Clang), a mensagem é error: use of undeclared identifier 'assert'.

Como resolver: #include <assert.h> no começo do arquivo.

Com -DNDEBUG (ou no modo Release), o programa passou a se comportar diferente

O que aconteceu: havia algo que precisava acontecer dentro de um assert (um scanf, um i++, uma chamada que muda algo), e ele sumiu junto com o assert (seção 3).

Como resolver: faça a ação fora e confira só o resultado: int lidos = scanf(…); assert(lidos == 1);, ou, melhor ainda para entrada do usuário, um if.

O teste nunca falha, nem quando o código está errado

O que aconteceu: o teste confere a coisa errada, por exemplo TESTE(dias_no_mes(2, 1900)) (qualquer número diferente de zero é "verdadeiro") em vez de TESTE(dias_no_mes(2, 1900) == 28).

Como resolver: veja cada teste falhar pelo menos uma vez: estrague a função de propósito (ou escreva o teste antes de consertar o erro) e confira que aparece FALHOU.

Dois main: múltiplas definições de "main"
$ gcc -std=c23 -Wall -Wextra main.c teste_datas.c datas.c -o programa
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccH62o2w.o: na função "main":
teste_datas.c:(.text+0x0): múltiplas definições de "main"; /tmp/ccHxhoAT.o:main.c:(.text+0x0): definido primeiro aqui
collect2: error: ld returned 1 exit status

O que aconteceu: você compilou o programa principal (main.c) junto com o de teste (teste_datas.c), e cada um tem o seu main. O ligador (ld) reclama: "múltiplas definições de main; definido primeiro aqui", no main.c (em inglês, multiple definition of `main'). Os nomes /tmp/cc….o são arquivos temporários e mudam a cada vez.

Como resolver: deixe as funções testáveis num arquivo sem main (datas.c) e faça dois executáveis: gcc main.c datas.c -o programa e gcc teste_datas.c datas.c -o teste_datas.


Referência rápida

ItemO que é
#include <assert.h>Traz o assert.
assert(condição);Se a condição for falsa: mensagem com arquivo, linha, função e condição, e abort() (código 134).
assert(cond && "texto");O texto aparece na mensagem.
-DNDEBUG ou #define NDEBUG antes do includeDesliga todos os assert.
static_assert(cond, "texto");Confere na compilação (veja static_assert).
#cond numa macroTransforma o argumento em string.
__FILE__, __LINE__, __func__Arquivo, linha e nome da função atuais.
enable_testing() / add_test(NAME n COMMAND prog)Testes no CMake.
ctest --test-dir build --output-on-failureRoda os testes e mostra a saída dos que falharam.

Exercícios

1. O cabeçalho

Complete o include necessário para usar assert:

#include <>

2. Desligar

Complete a opção do gcc que desliga todos os assert:

gcc -std=c23 -Wall -Wextra prog.c -o prog

3. Código de saída

Um programa terminou com Assertion `x > 0' failed. e Abortado. Qual o código de saída (echo $?)?

4. Um teste

Complete o teste que confere que fevereiro de 2023 tem 28 dias:

TESTE(dias_no_mes(2, 2023) );

5. Qual destes é um bom uso de assert?

6. Por que a macro TESTE é melhor que uma lista de assert para testes?

7. Qual destas funções é mais fácil de testar?


Resumo

  • assert(cond) (da assert.h) confere uma certeza do programador: se for falsa, mostra arquivo, linha, função e condição, e aborta (código 134).
  • -DNDEBUG desliga os assert (o modo Release do CMake faz isso). Por isso: nada que precise acontecer dentro deles, e nada de usá-los para validar a entrada do usuário.
  • Teste automático = um programa separado que chama as funções com exemplos de resposta conhecida. Uma macro TESTE(cond) com #cond, __FILE__ e __LINE__ confere todos, conta as falhas e devolve 0 ou 1.
  • Funções puras (só parâmetros e return) são as mais fáceis de testar: separe a conta da entrada e saída.
  • Teste os casos comuns, as bordas e cada erro já encontrado. Veja cada teste falhar uma vez.
  • CTest (enable_testing, add_test, ctest) roda tudo; Unity, CMocka e Criterion são frameworks prontos.