Teste — Arduino e IoT — semana 12 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 12 · Teste — Material de Apoio Arduino

Semana 12 de 16· 2o trimestre · 01/05 a 04/09

Teste

Descobrir que quebrou antes de o professor falar.

Aula 1 — Teste automatico de uma função pura

Objetivos

  • Reconhecer a função pura no seu próprio firmware, e dizer por que ela é a única que dá para testar sem bancada.
  • Escrever o primeiro teste unitário, com node:test, describe, it e assert.
  • Escrever o primeiro teste automático com node:test, com nome de caso, entrada e resultado esperado.
  • Ver um teste passar e ver um teste falhar, e entender que os dois são o mesmo trabalho.
  • Descobrir que um teste de borda trava uma decisão que ninguém tinha percebido que era uma decisão.
  • Medir quanto tempo leva para rodar a suíte, e entender por que esse número decide se o teste entra no push.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 DHT11 por dupla, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3
  • 1 computador com Node.js 20, por dupla
  • A camada de regra do projeto, do dia 10 aula 2
  • Folha de papel por dupla, para a tabela dos nove casos
  • Projetor, para o terminal do Node e o monitor serial da placa juntos

Conceitos

O que o teste faz é comparar com uma decisão escrita antes

Um teste é uma comparação entre o que a função devolve e o que alguém decidiu que ela deveria devolver. É essa comparação que se escreve uma vez e se roda muitas vezes, e é por isso que ela é um teste automático: não há ninguém apertando botão para ele acontecer. A verificação é o assert, que faz a comparação e interrompe tudo se ela falhar. Não há mais nada: quem escreve teste não escreve teste para verificar se teste funciona.

O que muda quando o teste passa é pequeno e o que muda quando ele falha é enorme. Passou: a decisão escrita antes ainda é a que o código faz, e a aula continua. Falhou: o código e o teste discordam, e alguém precisa descobrir quem está errado. A resposta honesta é sempre "descobrir quem está errado", e o teste não dá o direito de escolher: ele mostra as duas colunas lado a lado, o que foi esperado e o que aconteceu, e o nome do caso diz em uma linha o que aquela linha de código deveria estar fazendo.

A distinção que o professor escreve na lousa, porque ela é o que separa esta aula do apertar de botão: o teste não é o que verifica o teste. Um teste passa porque a entrada e o resultado esperado estão escritos antes de rodar. Se alguém escreve o resultado esperado olhando a saída do código, não está testando: está fotografando.

Função pura é a definição operacional de testável sem hardware

Uma função pura é uma função que dá o mesmo resultado para a mesma entrada. Só. Nada mais. O termo em inglês é *pure function*, e o nome completo em português é função que dá o mesmo resultado, que é mais comprido e mais honesto. A consequência prática vem logo depois, e é a que interessa: a função pura não depende de nada além dos seus argumentos, então testá-la não exige nada além dos argumentos.

Isso é o que permite testar sem hardware, e o termo vale para a placa inteira: o teste unitário da camada de regra não pede placa, não pede bancada, não pede cabo. A função avaliarLeitura recebe dois float e devolve um veredito. Ela não lê o DHT11, não usa WiFi, não mexe no relógio, não abre conexão e não escreve no Serial. Por isso o teste roda na máquina do professor, na sua, e na do colega do outro grupo, e dá o mesmo resultado nas três.

O que quebra essa propriedade, na ordem em que a turma costuma quebrar:

A função chamaPor que quebra a purezaComo o teste falha
sensor.readTemperature()o resultado depende do fio, do 3V3 e da bancadapassa na sua bancada, falha na do professor
millis() ou now()o resultado depende de quando rodoupassa hoje, falha amanhã
uma conexão de bancoo resultado depende do que tem gravadopassa com a tabela vazia, falha com ela cheia
console.loggrava fora, e o teste não limpapassa e deixa lixo
Math.random()resultado diferente a cada chamadafalha sempre, e ninguém entende por quê

A Serial está na lista e é o caso mais interesante para esta turma, porque parece inofensivo. Uma função que também escreve no Serial não é pura, e o motivo não é o texto que sai: é que o teste passa a precisar de um Serial falso para não sujar a tela, e um Serial falso é um mock, e mock é a coisa que o dia 12 aula 1 existe para não precisar. A regra do professor é simples: se o teste precisa de alguma coisa que não é argumento, a função está na camada errada.

node:test, assert, e o que cada um faz

O teste no material é escrito com node:test e com assert, e a division é esta:

PeçaO que éPara que serve
test(nome, funcao)o casonomeia a verificação e a executa
describe(nome, funcao)o grupojunta casos de uma mesma regra
assert.equal(obtido, esperado)a verificaçãocompara e interrompe se divergir
assert.ok(condicao)a verificação booleanapara o caso que só precisa ser verdadeiro

A função passada para test recebe nada, no material, e isso é proposital: avaliarLeitura tem dois argumentos e o teste os passa com valores literais dentro da verificação. Um teste com variável externa — "const t = lerSensor(); test(..., () => avaliarLeitura(t))" — é teste dependente, e o dia 12 aula 2 vai mostrar o que acontece com ele.

O assert tem uma propriedade que o professor usa bastante durante a aula: ele interrompe na primeira falha daquela verificação e imprime as duas colunas, o actual e o expected. A mensagem que apareceu na demonstração é literalmente isso, e vale ler em voz alta para a turma:

AssertionError [ERR_ASSERTION]: em 32,0 exatos o alerta nao dispara
  true !== false

Esse par true !== false é a aula inteira em uma linha: o que a regra devolveu, o que o teste queria, e nada mais. O nome do caso diz por que isso importa, e o professor repete que o nome do teste é documentação que ninguém vai ter tempo de escrever depois.

O teste que falha é o produto da aula

A demonstração que o professor faz é pequena e é a parte mais importante dos cinquenta minutos. Ele pega a regra, troca > por >= em um caractere, roda a suíte e mostra um caso vermelho:

✔ leitura normal e aceita e nao alerta
✔ calor acima do limite e aceito E alerta
✖ no limite exato o alerta ainda nao liga, porque a regra e ">" e nao ">=" (2.4ms)
  AssertionError: em 32,0 exatos o alerta nao dispara
  true !== false

Oito casos continuam verdes e um fica vermelho. É esse um para oito que o professor quer que a turma memorize: teste não é o que confirma que está certo, é o que pega a diferença entre o que se pensava e o que se faz. Uma suíte que passa de primeira não fez o trabalho dela; ela só disse que ninguém mudou nada desde a última vez.

E a inversão que ele escreve na lousa: quando o teste falha, o código está errado, não o teste. A regra vale porque o resultado esperado foi escrito antes. Se o teste falhou, alguém mudou a regra depois de escrevê-lo, e a pergunta não é "o teste está ruim?", é "a regra mudou de propósito?".

O caso de borda é o que trava a decisão

O caso do limite exato é o melhor exemplo do trimestre inteiro, e o professor conta por que ele o escreveu.

A regra diz em_alerta = temperatura_c > 32.0. Isso significa que em 32,0 exatos o alerta não liga. Ninguém na sala tinha notado essa decisão sendo tomada: ela estava escondida em um caractere do operador de comparação, escrita num dia em que o professor estava com pressa. O teste com a entrada 32,0 é o que transformou essa decisão implícita em decisão explícita, e a segunda metade da aula é sobre o que fazer com a decisão uma vez que ela é visível.

A razão pela qual a decisão importa de verdade aparece no dia 13 aula 2, e o professor não esconde: com > puro, uma temperatura oscilando em torno de 32,0 liga e desliga o alerta a cada leitura. Chama-se alerta oscilante, e é pior do que não ter alerta, porque o painel pisca e ninguém olha mais. A correção é a histerese: o alerta liga acima de 32,0 e só desliga abaixo de 30,0, com uma faixa no meio onde o estado anterior manda. O teste do limite exato é o que vai medir se a histerese está correta, e ele é escrito hoje.

O teste que falha tem uma cousin: o teste que passa e não deveria. O professor escreve o caso de NaN ao lado do caso de zero, e a verificação é a soma deles. A regra recusa NaN; a regra aceita zero, porque zero é um número. Um teste que só verifica "recusou o NaN" passaria mesmo com uma regra que recusa zero também, e esse é o buraco que a verificação comparada fecha.

O mesmo alvo, dois lugares onde ele roda

O ponto final é de arquitetura, e liga esta aula com a aula 2.

A suíte roda no Node, e mede a camada de regra do servidor. O sketch da resolução roda a mesma função, com o mesmo nome de caso, com a mesma entrada e o mesmo resultado esperado, dentro da placa, escrevendo no Serial. Não é teste substituindo teste: é a mesma decisão, verificada nos dois lugares onde a decisão vai valer.

E a consequência prática fecha com a aula 2: o teste de Node roda em milissegundos e entra em todo push; o teste na placa leva segundos, depende de cabo e de bancada, e roda antes da aula. Os dois verificam a mesma coisa, e por isso podem ter frequências completamente diferentes.

Atividade

Montagem:

  • DHT11 no GPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3.
  • Cabo USB conectado, monitor serial aberto em 115200.
  1. Abra a camada de regra do seu projeto e liste as funções que recebem valores e devolvem veredito, sem ler sensor, sem usar relógio e sem gravar. Qual delas é a sua função pura? Marque no código a linha exata.
  2. Escreva um teste para cada uma destas entradas da sua função: leitura normal, calor acima do alerta, limite exato do alerta, frio abaixo do mínimo, valor absurdo, umidade acima de 100, umidade negativa, sensor sem resposta. São oito casos; cole no caderno a tabela de nome, entrada e esperado.
  3. Rode a suíte e cole a linha de resumo com o número de testes, o de aprovados e o tempo total. Quanto tempo levou, em milissegundos?
  4. Agora mude o operador de comparação do alerta de > para >= no seu código. Rode a suíte de novo e cole a saída. Quantos casos passaram e quantos falharam? Copie a linha do erro.
  5. Volte o operador para >. Escreva um caso de borda para o seu projeto: qual é a entrada que está exatamente no limite de uma das suas regras, e o que você decidiu que acontece nela?
  6. Escreva um teste que prove que NaN e zero são casos diferentes na sua regra, e não apenas que os dois são recusados. Explique em uma frase o que o primeiro teste deixaria passar.
  7. Grave na placa o sketch da resolução e leia a saída. Compare, caso a caso, o que o Serial mostrou com o que o node --test mostrou na atividade 2. Algum caso discordou? O que isso diz sobre o sensor?
  8. Em uma frase: por que essa suíte pode rodar em todo push e a que roda na placa não?

Nota: 12 pontos. Critério de fim: os oito casos escritos com nome, entrada e esperado, e a saída vermelha da atividade 4 colada no caderno.

Resolucao

Do lado do Node, a suíte completa roda com node --test. Este foi executado nesta máquina com Node.js 24:

// dia 12 aula 1, lado do Node: o teste unitario de verdade.
// Rodado com: node --test d12a1.test.js
const test = require('node:test');
const assert = require('node:assert/strict');

// ===========================================================================
// A FUNCAO SOB TESTE. Copiada linha por linha da camada 2 do dia 10 aula 2.
// E a MESMA. Se a regra mudar no servidor, muda aqui tambem, e e por isso
// que o teste que quebra e o codigo — nao o teste.
// ===========================================================================
const LIMITE_ALERTA_C = 32.0;
const MIN_TEMP_C = -40.0;
const MAX_TEMP_C = 80.0;
const MIN_UMID_PCT = 0.0;
const MAX_UMID_PCT = 100.0;

function avaliarLeitura(temperatura_c, umidade_pct) {
  if (Number.isNaN(temperatura_c)) return { aceito: false, em_alerta: false, motivo: 'sensor devolveu NaN' };
  if (Number.isNaN(umidade_pct)) return { aceito: false, em_alerta: false, motivo: 'umidade devolveu NaN' };
  if (temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) {
    return { aceito: false, em_alerta: false, motivo: 'temperatura fora de faixa' };
  }
  if (umidade_pct < MIN_UMID_PCT || umidade_pct > MAX_UMID_PCT) {
    return { aceito: false, em_alerta: false, motivo: 'umidade fora de faixa' };
  }
  const em_alerta = temperatura_c > LIMITE_ALERTA_C;
  return {
    aceito: true, em_alerta,
    motivo: em_alerta ? 'aceito, acima do limite' : 'aceito, dentro do esperado',
  };
}

// ===========================================================================
// O TESTE. Um test por caso, com o nome do caso, a entrada e o esperado.
// Repare que NAO existe mock, nem banco, nem servidor, nem relogio. A
// funcao recebe dois numeros e devolve um veredito, e o teste termina.
// ===========================================================================
test('leitura normal e aceita e nao alerta', () => {
  const v = avaliarLeitura(27.4, 61.0);
  assert.equal(v.aceito, true);
  assert.equal(v.em_alerta, false);
  assert.equal(v.motivo, 'aceito, dentro do esperado');
});

test('calor acima do limite e aceito E alerta: gravar e avisar sao duas respostas', () => {
  const v = avaliarLeitura(34.8, 55.0);
  assert.equal(v.aceito, true, '34,8 C e um numero valido e tem de ser gravado');
  assert.equal(v.em_alerta, true, '34,8 C esta acima do limite de alerta');
});

test('no limite exato o alerta ainda nao liga, porque a regra e ">" e nao ">="', () => {
  const v = avaliarLeitura(32.0, 50.0);
  assert.equal(v.aceito, true);
  assert.equal(v.em_alerta, false, 'em 32,0 exatos o alerta nao dispara');
});

test('frio abaixo de -40 e recusado', () => {
  assert.equal(avaliarLeitura(-55.0, 40.0).aceito, false);
});

test('900 C e recusado como dado corrompido', () => {
  const v = avaliarLeitura(900.0, 61.0);
  assert.equal(v.aceito, false);
  assert.equal(v.motivo, 'temperatura fora de faixa');
});

test('umidade acima de 100 e recusada', () => {
  assert.equal(avaliarLeitura(27.0, 140.0).aceito, false);
});

test('umidade negativa e recusada', () => {
  assert.equal(avaliarLeitura(27.0, -5.0).aceito, false);
});

test('sensor sem resposta (NaN) e recusado, e nao tratado como zero', () => {
  const v = avaliarLeitura(NaN, 61.0);
  assert.equal(v.aceito, false);
  assert.equal(v.motivo, 'sensor devolveu NaN');
  assert.notEqual(v.aceito, avaliarLeitura(0, 61.0).aceito, 'NaN e 0 nao podem ser o mesmo caso');
});

test('a mesma entrada devolve a mesma saida, mil vezes seguidas', () => {
  const primeiro = JSON.stringify(avaliarLeitura(34.8, 55.0));
  for (let i = 0; i < 1000; i++) {
    assert.equal(JSON.stringify(avaliarLeitura(34.8, 55.0)), primeiro,
      a chamada ${i + 1} devolveu algo diferente das anteriores);
  }
});

A saída real, medida nesta máquina:

✔ leitura normal e aceita e nao alerta (2.111433ms)
✔ calor acima do limite e aceito E alerta: gravar e avisar sao duas respostas (0.568278ms)
✔ no limite exato o alerta ainda nao liga, porque a regra e ">" e nao ">=" (0.45123ms)
✔ frio abaixo de -40 e recusado (0.264793ms)
✔ 900 C e recusado como dado corrompido (0.530598ms)
✔ umidade acima de 100 e recusada (0.271276ms)
✔ umidade negativa e recusada (13.039989ms)
✔ sensor sem resposta (NaN) e recusado, e nao tratado como zero (0.541909ms)
✔ a mesma entrada devolve a mesma saida, mil vezes seguidas (13.984069ms)
ℹ tests 9
ℹ suites 0
ℹ pass 9
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 306.357267

E agora, com o código quebrado de propósito — o operador > virou >=, uma tecla:

entrada 32,0 C / 50 % — o mesmo caso, duas versoes da regra:
  versao correta (>):  aceito=true, alerta=false
  versao com bug (>=): aceito=true, alerta=true

Uma tecla trocada. O alerta passa a ligar no limite exato, e o
teste que exige alerta=false nesse caso falha em milissegundos.
✔ leitura normal e aceita e nao alerta (2.914108ms)
✔ calor acima do limite e aceito E alerta (19.416587ms)
✖ no limite exato o alerta ainda nao liga, porque a regra e ">" e nao ">=" (2.433793ms)
✔ frio abaixo de -40 e recusado (0.38612ms)
✔ 900 C e recusado como dado corrompido (0.32748ms)
✔ umidade acima de 100 e recusada (0.457442ms)
✔ umidade negativa e recusada (0.669236ms)
✔ sensor sem resposta (NaN) e recusado (0.426414ms)
✔ a mesma entrada devolve a mesma saida, mil vezes seguidas (2.919268ms)
ℹ tests 18
ℹ pass 17
ℹ fail 1

✖ failing tests:

test at quebrado.js:51:1
✖ no limite exato o alerta ainda nao liga, porque a regra e ">" e nao ">=" (2.433793ms)
  AssertionError [ERR_ASSERTION]: em 32,0 exatos o alerta nao dispara
  true !== false
      at TestContext.<anonymous> (/root/.hermes/cache/scratch/aula/quebrado.js:54:10)

O professor aponta quatro coisas nessa segunda saída:

  • fail 1 de tests 18: oito casos continuam verdes e um fica vermelho. É esse um para oito que a turma precisa entender: o teste não confirma que está certo, ele pega a diferença entre o que se pensava e o que se faz.
  • true !== false: o que a regra devolveu, o que o teste queria, e nada mais. A mensagem inteira cabe em uma linha porque o nome do caso já disse por que aquilo importa.
  • O code: 'ERR_ASSERTION' com o arquivo e a linha: quebrado.js:54:10. O teste não só diz que falhou: diz onde. Quem não leu o nome do caso consegue abrir a linha 54 e ver o > que virou >=.
  • 2.433793ms no caso que falhou: o tempo é irrelevante para a correção e é o argumento principal para o push. Um teste que leva milissegundos pode rodar sempre; um que leva segundos, não.

O sketch da placa, que é o mesmo alvo verificado em outro lugar:

// dia 12, aula 1: a funcao pura, isolada, testada.
//
// Este sketch existe por um motivo so: mostrar que existe UMA funcao no
// firmware que o professor vai escrever o teste no Node, e que ela e
// exatamente a mesma funcao.
//
// A funcao e `avaliarLeitura`. Ela nao le sensor, nao fala com a rede, nao
// mexe no banco e nao depende de relogio. Dada a mesma entrada, ela devolve
// sempre a mesma saida. E isso que a permite rodar em 40 ms numa maquina
// sem placa, sem rede e sem banco.
//
// O teste unitario do dia 12 aula 2 roda contra o arquivo da CAMADA 2 do
// dia 10 aula 2 — nao contra este arquivo. Mas a regra e a mesma linha
// por linha, e o professor mostra as duas coladas na lousa.

#include <Arduino.h>
#include <DHT.h>

const int PIN_DHT = 4;
const uint8_t TIPO_SENSOR = DHT11;

// ===========================================================================
// A FUNCAO SOB TESTE
// Nao chama nada, nao le nada, nao grava nada. E por isso que o teste
// roda sem hardware: se ela lesse o sensor, o teste so passaria na
// bancada do aluno e passaria por acidente na maquina do professor.
// ===========================================================================

const float LIMITE_ALERTA_C = 32.0f;
const float MIN_TEMP_C = -40.0f;
const float MAX_TEMP_C = 80.0f;
const float MIN_UMID_PCT = 0.0f;
const float MAX_UMID_PCT = 100.0f;

// Retorna um veredito: aceitar, recusar, e se esta em alerta.
// Alertar e gravar sao respostas diferentes para o mesmo dado.
struct Veredito {
  bool aceito;
  bool em_alerta;
  const char* motivo;
};

Veredito avaliarLeitura(float temperatura_c, float umidade_pct) {
  Veredito v;

  if (isnan(temperatura_c)) {
    v.aceito = false; v.em_alerta = false; v.motivo = "sensor devolveu NaN";
    return v;
  }
  if (isnan(umidade_pct)) {
    v.aceito = false; v.em_alerta = false; v.motivo = "umidade devolveu NaN";
    return v;
  }
  if (temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) {
    v.aceito = false; v.em_alerta = false; v.motivo = "temperatura fora de faixa";
    return v;
  }
  if (umidade_pct < MIN_UMID_PCT || umidade_pct > MAX_UMID_PCT) {
    v.aceito = false; v.em_alerta = false; v.motivo = "umidade fora de faixa";
    return v;
  }

  v.aceito = true;
  v.em_alerta = temperatura_c > LIMITE_ALERTA_C;
  v.motivo = v.em_alerta ? "aceito, acima do limite" : "aceito, dentro do esperado";
  return v;
}

// ===========================================================================
// O "TESTE", escrito em C++ para rodar NA PLACA.
//
// Isto nao substitui o `node --test` do dia 12 aula 2: aqui o firmware
// roda na placa, e o teste roda na sua maquina. Mas o ALVORO e o mesmo —
// nome do caso, entrada, resultado esperado — e o professor mostra que
// mudar o limite e o codigo quebrar e nao o teste. E o sinal de que o
// teste esta medindo a coisa certa.
// ===========================================================================

int testesRodados = 0;
int testesPassaram = 0;

void verificar(const char* nome, Veredito obtido, bool aceitoEsperado, bool alertaEsperado) {
  testesRodados++;
  bool passou = (obtido.aceito == aceitoEsperado) && (obtido.em_alerta == alertaEsperado);
  if (passou) testesPassaram++;
  Serial.print(passou ? "  ok   " : "  FALHOU ");
  Serial.print(nome);
  Serial.print("  -> aceito=");
  Serial.print(obtido.aceito ? "sim" : "nao");
  Serial.print(", alerta=");
  Serial.print(obtido.em_alerta ? "sim" : "nao");
  Serial.print(" (esperado aceito=");
  Serial.print(aceitoEsperado ? "sim" : "nao");
  Serial.print(", alerta=");
  Serial.println(alertaEsperado ? "sim" : "nao");
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 12 aula 1 — teste automatico de uma funcao pura");
  Serial.println("===================================================");
  Serial.println("A funcao sob teste e avaliarLeitura(temperatura_c, umidade_pct).");
  Serial.println("Ela nao le sensor, nao usa rede, nao mexe em relogio nem banco.");
  Serial.println();

  Serial.println("--- os casos que o teste do dia 12 escreve ---");
  verificar("leitura normal",       avaliarLeitura(27.4f, 61.0f),  true,  false);
  verificar("calor, acima do alerta", avaliarLeitura(34.8f, 55.0f), true,  true );
  verificar("limite exato do alerta", avaliarLeitura(32.0f, 50.0f), true,  false);
  verificar("frio demais",           avaliarLeitura(-55.0f, 40.0f), false, false);
  verificar("calor absurdo",         avaliarLeitura(900.0f, 61.0f),  false, false);
  verificar("umidade acima de 100",  avaliarLeitura(27.0f, 140.0f),  false, false);
  verificar("umidade negativa",      avaliarLeitura(27.0f, -5.0f),   false, false);
  verificar("sensor sem resposta",   avaliarLeitura(NAN, 61.0f),     false, false);

  Serial.println();
  Serial.print("passaram ");
  Serial.print(testesPassaram);
  Serial.print(" de ");
  Serial.print(testesRodados);
  Serial.println(" casos");

  Serial.println();
  Serial.println("--- a mesma funcao, agora com o sensor de verdade ---");
  DHT sensor(PIN_DHT, TIPO_SENSOR);
  sensor.begin();
  float t = sensor.readTemperature();
  float u = sensor.readHumidity();
  Serial.print("banco: ");
  Serial.print(t, 1);
  Serial.print(" C / ");
  Serial.print(u, 0);
  Serial.println(" %");
  Veredito real = avaliarLeitura(t, u);
  Serial.print("veredito: aceito=");
  Serial.print(real.aceito ? "sim" : "nao");
  Serial.print(", alerta=");
  Serial.print(real.em_alerta ? "sim" : "nao");
  Serial.print(" — ");
  Serial.println(real.motivo);
  Serial.println("Se o veredito vier 'nao' aqui, a funcao esta certa e o");
  Serial.println("sensor esta sem resposta. O teste nao falhou: ele provou");
  Serial.println("que a funcao recusa NaN. E o que o teste e para isso.");

  Serial.println();
  Serial.println("--- por que 'limite exato' e um caso e nao um detalhe ---");
  Serial.println("  Em 32,0 C o alerta ainda NAO liga, porque a regra e '>' e");
  Serial.println("  nao '>='. O dia 13 aula 2 usa isso para a histerese: um alerta");
  Serial.println("  que liga e desliga a cada leitura e pior que nenhum alerta.");
  Serial.println("  Escrever esse caso no teste e o que trava a decisao.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Por que assim e não de outro jeito. A Veredito é uma struct com três campos e sem construtor, e isso é deliberado. Ela funciona aqui porque avaliarLeitura preenche os três campos em todos os caminhos de retorno: cada if faz as três atribuições e volta. Se um if novo fosse acrescentado no meio sem as três linhas, o campo ficaria com lixo de memória. O dia 10 aula 2 resolveu isso com construtor; aqui o professor explica que as duas respostas são válidas e que a segurança vem da disciplina de preenchimento, que o teste comprova: se um caminho esquecesse um campo, o caso correspondente apareceria com um valor aleatório.

O motivo é const char* e não uma String. O dia 5 do 1o trimestre mostrou que a String do Arduino aloca memória na heap e que alocar num laço de dez mil iterações fragmenta o heap até a placa reiniciar. Com const char* apontando para literais que estão na flash, o custo é zero. O professor pede que a turma explique por que motivo pode ser const char* aqui e não pode ser um ponteiro para memória que a função cria: o ponteiro seria devolvido apontando para memória já liberada, e o Serial.println leria lixo.

A função verificar é o assert em C++, e a assinatura mostra a decisão de projeto que importa: ela recebe o resultado obtido e os dois resultados esperados, e compara os três. A comparação é && entre duas condições, e não == entre objetos. A regra é: compare campo a campo, nunca o objeto inteiro. Se alguém tentasse comparar a Veredito com ==, o compilador daria erro, e o motivo é que a comparação de estrutura depende de como a estrutura foi implementada — o teste ficaria quebrado só porque alguém adicionou um campo. Comparar aceito e em_alerta separadamente é o que mantém o teste dizendo a verdade depois de uma mudança na struct.

O NAN como argumento da oitava verificação é o mesmo argumento da aula 8, e ele está aqui por um motivo diferente: o teste precisa de um NaN que a placa possa não ter. Na bancada real, quando o DHT11 falha, ele devolve NaN — mas nem sempre, e nunca no momento em que o teste roda. Passando NAN na mão, o caso é reprodutível em qualquer placa, em qualquer dia, e é isso que um teste precisa ser.

A leitura do sensor no fim de setup é a conferência de sanidade, e ela é o que separa teste de demonstração. O Serial mostra a leitura real, aplica a mesma função e imprime o veredito. Se o DHT11 estiver sem resposta, o veredito vem não com motivo sensor devolveu NaN, e o aluno vê que os oito casos acima passaram e o sensor está desligado ao mesmo tempo. São coisas compatíveis, e a suíte não mentiu: ela verificou a função, e a função está certa.

O testesRodados e testesPassaram são globais porque a C++ não tem closures como o JavaScript. O node --test agrupa os casos em um objeto e guarda o estado dele; aqui o estado precisa de duas variáveis de escopo de arquivo. O professor assume isso em voz alta, porque "por que a placa precisa de global e o Node não" é uma pergunta de turma e a resposta é boa: cada lado tem o que o seu ambiente oferece.

Criterios de correcao

CritérioPontos
Função pura do projeto identificada, com a linha do código marcada2 pontos
Os oito casos escritos com nome, entrada e resultado esperado, na tabela do caderno3 pontos
Item 3: a linha de resumo colada, com número de testes, aprovados e tempo em milissegundos1 pontos
Item 4: o operador trocado, a suíte rodada e a linha do erro vermelha colada2 pontos
Caso de borda do projeto escrito, com a entrada no limite e a decisão tomada2 pontos
Teste que distingue NaN de zero escrito, e a explicação do que o primeiro deixaria passar1 pontos
Item 7: comparação caso a caso entre o Serial e o node --test, com o caso que discordou1 pontos

Erros comuns

ErroComo apareceCorreção
Testar a função que lê o sensor"Escrevi o teste chamando avaliarLeitura(sensor.readTemperature())""Esse teste passa na sua bancada e falha na do professor, e ninguém descobre por quê. Teste a função que recebe o valor; a que lê o sensor não é pura."
Usar millis() ou now() na função"O teste passou ontem e falhou hoje""A função pura não depende de quando roda. Se ela pergunta a hora ou ao millis, ela deixou de ser pura, e a flutuação do tempo entrou no teste."
Escrever o esperado olhando a saída"Rodei, vi que deu aceito, e escrevi aceito""Isso fotografa o código, não testa. O resultado esperado é uma decisão escrita antes. Se você escreve olhando a saída, o teste nunca falha, e nunca vai valer nada."
Não ter nenhum caso de borda"Passei em todos os casos""Todos os casos que você escreveu passam, e nenhum deles está no limite. O caso de borda é o que trava a decisão do > contra o >=, e é o que pega a regressão do dia 13."
Esperar alert = true em NaN"Fiz o teste esperando que NaN dê alerta""NaN é recusado, não alertado. São respostas diferentes: aceito é se o dado presta, em_alerta é se ele é quente. Recusado nunca acende alerta."
Comparar o objeto inteiro"Tentei assert.equal(v, {aceito:true}) e não funciona""A comparação de objeto depende de como o objeto foi construído. Compare campo a campo: assert.equal(v.aceito, true). E o teste sobrevive a alguém acrescentar um campo."
Suíte sem nomes descritivos"Chamei o teste de 'teste 1'""O nome do caso é a documentação. Quando a suíte falha, o nome é a primeira coisa que se lê, e é ele que diz por que aquela linha importa. Escrever 'teste 1' obriga a abrir o arquivo."
Ignorar o caso que falha"Quebrei o código, vi que falhou, e voltei""Esse é o momento da aula. Olhe o nome do caso, a linha do erro e o true !== false. O teste disse onde está a diferença entre o que você pensou e o que você fez."
Não ter nenhum teste para 0"Só testei NaN""Um teste que só recusa NaN passa mesmo com uma regra que recusa zero também. Escreva os dois no mesmo caso, comparando: NaN é recusado, 0 é aceito, e são casos diferentes."
Concluir que o teste garante o sistema inteiro"Escrevi um teste e agora o sistema está coberto""Isto cobre uma função. WiFi, sensor na protoboard e relógio do servidor continuam sem cobertura, e continuam quebrando. Cobertura de uma camada é o que a aula 1 faz; o resto é a aula 2."

Desafio extra

Escreva o teste da histerese que o dia 13 aula 2 vai precisar, ainda sem implementar a histerese: o teste deve afirmar que, com o alerta ligado acima de 32,0, uma leitura de 31,0 mantém o alerta ligado, e uma de 29,0 o desliga. Rode-o agora: ele falha, porque a regra atual não tem faixa intermediária. Depois implemente a histerese mínima e rode de novo. O que o teste prova no fim, e o que o professor mede, é que a decisão da faixa de 30,0 a 32,0 está escrita em algum lugar do projeto, e não implícita em um operador de comparação.

>

A resolucao, compilada

// dia 12, aula 1: a funcao pura, isolada, testada.
//
// Este sketch existe por um motivo so: mostrar que existe UMA funcao no
// firmware que o professor vai escrever o teste no Node, e que ela e
// exatamente a mesma funcao.
//
// A funcao e `avaliarLeitura`. Ela nao le sensor, nao fala com a rede, nao
// mexe no banco e nao depende de relogio. Dada a mesma entrada, ela devolve
// sempre a mesma saida. E isso que a permite rodar em 40 ms numa maquina
// sem placa, sem rede e sem banco.
//
// O teste unitario do dia 12 aula 2 roda contra o arquivo da CAMADA 2 do
// dia 10 aula 2 — nao contra este arquivo. Mas a regra e a mesma linha
// por linha, e o professor mostra as duas coladas na lousa.

#include <Arduino.h>
#include <DHT.h>

const int PIN_DHT = 4;
const uint8_t TIPO_SENSOR = DHT11;

// ===========================================================================
// A FUNCAO SOB TESTE
// Nao chama nada, nao le nada, nao grava nada. E por isso que o teste
// roda sem hardware: se ela lesse o sensor, o teste so passaria na
// bancada do aluno e passaria por acidente na maquina do professor.
// ===========================================================================

const float LIMITE_ALERTA_C = 32.0f;
const float MIN_TEMP_C = -40.0f;
const float MAX_TEMP_C = 80.0f;
const float MIN_UMID_PCT = 0.0f;
const float MAX_UMID_PCT = 100.0f;

// Retorna um veredito: aceitar, recusar, e se esta em alerta.
// Alertar e gravar sao respostas diferentes para o mesmo dado.
struct Veredito {
  bool aceito;
  bool em_alerta;
  const char* motivo;
};

Veredito avaliarLeitura(float temperatura_c, float umidade_pct) {
  Veredito v;

  if (isnan(temperatura_c)) {
    v.aceito = false; v.em_alerta = false; v.motivo = "sensor devolveu NaN";
    return v;
  }
  if (isnan(umidade_pct)) {
    v.aceito = false; v.em_alerta = false; v.motivo = "umidade devolveu NaN";
    return v;
  }
  if (temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) {
    v.aceito = false; v.em_alerta = false; v.motivo = "temperatura fora de faixa";
    return v;
  }
  if (umidade_pct < MIN_UMID_PCT || umidade_pct > MAX_UMID_PCT) {
    v.aceito = false; v.em_alerta = false; v.motivo = "umidade fora de faixa";
    return v;
  }

  v.aceito = true;
  v.em_alerta = temperatura_c > LIMITE_ALERTA_C;
  v.motivo = v.em_alerta ? "aceito, acima do limite" : "aceito, dentro do esperado";
  return v;
}

// ===========================================================================
// O "TESTE", escrito em C++ para rodar NA PLACA.
//
// Isto nao substitui o `node --test` do dia 12 aula 2: aqui o firmware
// roda na placa, e o teste roda na sua maquina. Mas o ALVORO e o mesmo —
// nome do caso, entrada, resultado esperado — e o professor mostra que
// mudar o limite e o codigo quebrar e nao o teste. E o sinal de que o
// teste esta medindo a coisa certa.
// ===========================================================================

int testesRodados = 0;
int testesPassaram = 0;

void verificar(const char* nome, Veredito obtido, bool aceitoEsperado, bool alertaEsperado) {
  testesRodados++;
  bool passou = (obtido.aceito == aceitoEsperado) && (obtido.em_alerta == alertaEsperado);
  if (passou) testesPassaram++;
  Serial.print(passou ? "  ok   " : "  FALHOU ");
  Serial.print(nome);
  Serial.print("  -> aceito=");
  Serial.print(obtido.aceito ? "sim" : "nao");
  Serial.print(", alerta=");
  Serial.print(obtido.em_alerta ? "sim" : "nao");
  Serial.print(" (esperado aceito=");
  Serial.print(aceitoEsperado ? "sim" : "nao");
  Serial.print(", alerta=");
  Serial.println(alertaEsperado ? "sim" : "nao");
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 12 aula 1 — teste automatico de uma funcao pura");
  Serial.println("===================================================");
  Serial.println("A funcao sob teste e avaliarLeitura(temperatura_c, umidade_pct).");
  Serial.println("Ela nao le sensor, nao usa rede, nao mexe em relogio nem banco.");
  Serial.println();

  Serial.println("--- os casos que o teste do dia 12 escreve ---");
  verificar("leitura normal",       avaliarLeitura(27.4f, 61.0f),  true,  false);
  verificar("calor, acima do alerta", avaliarLeitura(34.8f, 55.0f), true,  true );
  verificar("limite exato do alerta", avaliarLeitura(32.0f, 50.0f), true,  false);
  verificar("frio demais",           avaliarLeitura(-55.0f, 40.0f), false, false);
  verificar("calor absurdo",         avaliarLeitura(900.0f, 61.0f),  false, false);
  verificar("umidade acima de 100",  avaliarLeitura(27.0f, 140.0f),  false, false);
  verificar("umidade negativa",      avaliarLeitura(27.0f, -5.0f),   false, false);
  verificar("sensor sem resposta",   avaliarLeitura(NAN, 61.0f),     false, false);

  Serial.println();
  Serial.print("passaram ");
  Serial.print(testesPassaram);
  Serial.print(" de ");
  Serial.print(testesRodados);
  Serial.println(" casos");

  Serial.println();
  Serial.println("--- a mesma funcao, agora com o sensor de verdade ---");
  DHT sensor(PIN_DHT, TIPO_SENSOR);
  sensor.begin();
  float t = sensor.readTemperature();
  float u = sensor.readHumidity();
  Serial.print("banco: ");
  Serial.print(t, 1);
  Serial.print(" C / ");
  Serial.print(u, 0);
  Serial.println(" %");
  Veredito real = avaliarLeitura(t, u);
  Serial.print("veredito: aceito=");
  Serial.print(real.aceito ? "sim" : "nao");
  Serial.print(", alerta=");
  Serial.print(real.em_alerta ? "sim" : "nao");
  Serial.print(" — ");
  Serial.println(real.motivo);
  Serial.println("Se o veredito vier 'nao' aqui, a funcao esta certa e o");
  Serial.println("sensor esta sem resposta. O teste nao falhou: ele provou");
  Serial.println("que a funcao recusa NaN. E o que o teste e para isso.");

  Serial.println();
  Serial.println("--- por que 'limite exato' e um caso e nao um detalhe ---");
  Serial.println("  Em 32,0 C o alerta ainda NAO liga, porque a regra e '>' e");
  Serial.println("  nao '>='. O dia 13 aula 2 usa isso para a histerese: um alerta");
  Serial.println("  que liga e desliga a cada leitura e pior que nenhum alerta.");
  Serial.println("  Escrever esse caso no teste e o que trava a decisao.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia12 aula1.

Aula 2 — Testar rota e banco com banco separado

Objetivos

  • Explicar por que o banco de teste é separado do banco da aula, e o que acontece quando ele não é.
  • Montar o estado limpo antes do primeiro caso, e entender por que DROP TABLE e CREATE TABLE vale mais que TRUNCATE aqui.
  • Escrever o teste que depende do anterior de propósito, e ver que ele passa na primeira execução e falha na segunda.
  • Subir a própria instância do servidor no teste, e explicar por que ela nunca chama listen(3000).
  • Separar, por escrito, o que o teste de rota prova e o que ele não prova.

Material

  • 1 computador com Node.js 20, mysql2 e acesso a um MySQL ou MariaDB 10.11, por dupla
  • 1 banco de teste criado para a turma, com nome diferente do banco da aula
  • A camada de rota e a camada de regra do projeto, do dia 10 aula 2
  • Folha de papel por dupla, com a tabela dos quatro casos
  • Projetor, para o terminal do Node e a saída do teste juntos

Conceitos

Por que o banco do teste é outro

O teste de integração precisa de uma coisa que o teste unitário não precisa: precisa apagar a tabela entre os casos. Sem estado limpo, o segundo caso lê a linha que o primeiro gravou e falha, e o teste passa a depender da ordem.

Apagar tabela, no banco da aula, é apagar o histórico que a turma construiu nas aulas 4 a 6. O professor escreve isso na lousa e não suaviza: não há como desfazer. Um DROP TABLE não tem Ctrl+Z.

Daí vem o banco separado, e a decisão de implementação é a parte que a turma costuma errar. O que o teste espera é um schema de teste, com o esquema já criado e as tabelas já semeado para teste: a definição do esquema, a carga inicial e a limpeza são feitas uma vez, antes da aula, e não a cada execução. Existem duas maneiras de separar, e só uma delas é segura:

ManeiraO que éPor que não serve
outra tabela no mesmo bancotb_t2_teste ao lado de tb_t2_leiturauma tabela se esquece; um TRUNCATE sem WHERE erra a tabela e apaga a da aula
outro schemamateriais_teste ao lado de materiais_producaoo USE do schema impede que qualquer TRUNCATE genérico caia na tabela errada

A regra banco de produção não se toca vale para o banco da aula exatamente como vale para o de produção: o teste é um visitante, e visitante não apaga a casa. E o mesmo vale para o schema: o que é de teste é semeado para teste, e o que é da aula é intocável.

O schema de teste é a mesma coisa que schema de produção, com nome diferente e dados descartáveis. O que o professor pede, na atividade, é que cada dupla olhe o nome do schema no log antes de qualquer coisa, e escreva no caderno. Não é superstição: é o que separa um teste que falha de um teste que destruiu dados.

Estado limpo: por que DROP e CREATE, e não TRUNCATE

O estado limpo significa que a tabela começa vazia em cada execução, e o motivo de precisar disso é o reset entre testes: sem ele, o caso seguinte herda o que o anterior deixou.

O caminho escolhido é DROP TABLE IF EXISTS seguido de CREATE TABLE, e ele parece mais pesado que TRUNCATE até o professor explicar o motivo. O TRUNCATE esvazia a tabela, mas não devolve o AUTO_INCREMENT: a contagem continua de onde parou. O DELETE sem WHERE tem o mesmo problema.

O efeito disso aparece no item 4 da atividade, e o professor roda na frente da turma. Um teste que verifica insertId === 1 passa na primeira execução, porque a tabela acabou de ser criada e o contador começa em 1. Na segunda execução, sem recriar a tabela, o insertId é 3, e o teste falha. O aluno olha o teste, olha o código, não vê nada de errado, e conclui que o teste é instável.

O nome do problema é ordem dos testes, e a variante mais comum do problema é um teste que depende de outro: um caso que só passa porque o caso anterior deixou a tabela no estado que ele precisa. O node --test roda os casos na ordem em que estão no arquivo, e roda em outra ordem quando um arquivo novo aparece na pasta. Aí o teste que passava começa a falhar sem que ninguém tenha escrito nada.

A correção tem duas partes, e o professor escreve as duas: cada teste monta o próprio dado, e a ordem não importa. Se o segundo caso precisa de uma linha na tabela, ele cria essa linha no começo dele, com o próprio gravarLeitura, em vez de confiar que o primeiro criou.

A instância do teste e a porta

O teste de rota sobe um servidor de verdade, faz requisição de verdade e recebe resposta de verdade. Isso é o que o diferencia do teste unitário: ele atravessa a rede, o que o torna um teste de integração — ele integra rota, regra, banco e serialização de JSON em uma execução.

A requisição de teste é feita por HTTP de verdade, com porta, cabeçalho e corpo. O detalhe que evita o desastre é a porta. O servidor da aula está em 3000, e o teste não pode chamar listen(3000): se chamasse, ele derrubaria o servidor que a turma está usando, e o teste passaria a depender de ter o servidor parado.

A solução é listen(0), que o professor explica como sendo o mesmo pedido do dia 7: "me dá uma porta livre". O sistema operacional escolhe uma porta que ninguém está usando, e o teste sobe e desce nela dentro da mesma execução. A saída medida mostra a porta que o sistema escolheu, e o número muda a cada execução — o que é a prova de que a porta é do teste e não do projeto.

O supertest faz essa mesma coisa por baixo, e o professor cita o nome porque ele é o que a turma vai encontrar em qualquer projeto: ele sobe a aplicação em uma porta efêmera, faz a requisição e derruba. Nenhum listen no projeto inteiro, e nenhum conflito de porta em trinta bancadas.

Sucesso e falha esperados, escritos antes

O sucesso e falha esperados é a frase que o professor usa para o par de valores que cada caso carrega, e ele escreve o formato na lousa:

status 201  e  { id: 1, em_alerta: false }      -> o dado entrou e nao esta em alerta
status 422  e  { erro: "temperatura fora de faixa" }  -> recusado, e o motivo e nomeado
status 401  e  { erro: "chave ausente ou invalida" }    -> sem chave, nem chegou na regra

Escrever o motivo no esperado, e não só o status, é a diferença entre um teste que diz o que aconteceu e um que diz apenas que aconteceu alguma coisa. O 422 com o motivo nomeado é o que garante que a recusa foi da regra e não do banco: se o motivo viesse vazio, o 422 poderia ser qualquer coisa.

O 401 merece atenção porque ele prova uma ordem. A rota checa a chave antes de chamar a regra, e o teste confirma isso: a requisição com dado bom e sem chave devolveu 401, e não gravou linha. Se a chave fosse conferida depois da gravação, o mesmo teste mostraria uma linha a mais na tabela, e o professor pede que a turma confira isso no item 2 da atividade.

O que o teste prova e o que ele não prova

A lista é a parte que o professor pede por escrito, e é a mesma nos dois lados da aula:

ProvaNão prova
o status de cada casose o WiFi da placa funciona
que o INSERT aconteceuse o DHT11 está na protoboard
que o 422 veio da regra, com motivose o relógio do servidor está certo
que o 401 acontece antes de gravarse a placa está mandando no intervalo certo
que o JSON de saída tem os camposque a bancada inteira funciona

A razão de a coluna da direita existir é a que o professor repete duas vezes: um teste que tenta provar tudo não prova nada, porque quando falha não diz onde. Um teste que verifica quatro status e um INSERT falha dizendo expected 422, got 500 com o arquivo e a linha, e alguém resolve em dez minutos. Um teste que verifica o sistema inteiro falha com "deu ruim" e ninguém sabe por onde começar.

A divisão que fecha a aula é a das duas aulas do dia: a aula 1 testou a função pura sem banco e sem servidor, e a aula 2 testou o caminho inteiro com banco e com servidor. Nenhuma das duas pega coisa de hardware, e a Reason é a mesma nas duas: o que depende de fio, de bancada e de relógio é verificado na bancada, antes da aula, e não no push.

Atividade

Montagem:

  • Nenhuma montagem nova. A placa fica sem sensor e sem rede nesta aula.
  • Banco de teste criado, com nome diferente do banco da aula, na máquina da dupla.
  1. Escreva no caderno o nome do schema do seu banco de teste e o do banco da aula. Marque no seu .env qual variável aponta para cada um, e diga qual delas o npm test deve ler.
  2. Escreva o before da sua suíte com o estado limpo. Rode o teste de rota com dado bom e depois com dado corrompido, e conte quantas linhas ficaram na tabela. O 422 gravou alguma linha?
  3. Rode o mesmo caso de dado bom duas vezes sem recriar a tabela, e anote o id devolvido em cada uma. Agora escreva um teste que exija id === 1 e rode a suíte duas vezes seguidas. Ele falha na segunda? O que o AUTO_INCREMENT tem a ver com isso?
  4. Troque o TRUNCATE do seu before por DROP TABLE IF EXISTS mais CREATE TABLE e rode a suíte duas vezes. O id voltou a 1 nas duas? O que a diferença entre TRUNCATE e DROP te diz sobre estado limpo?
  5. Verifique a ordem: no seu teste de requisição sem chave, o dado bom também entrou na tabela? Se entrou, sua rota está conferindo a chave depois de gravar, e a linha do dia 9 aula 2 está fora do lugar.
  6. Escreva um caso que dependa do anterior de propósito, e um caso que monte o próprio dado. Rode a pasta com os dois casos em ordens trocadas usando a flag de ordenação do runner. O que acontece com o primeiro, e o que acontece com o segundo?
  7. Escreva a lista do que o seu teste de rota prova e do que ele não prova, em duas colunas, e cole no caderno. Cite da coluna da direita pelo menos dois itens que dependem da bancada.
  8. Em uma frase: por que o teste sobe a própria instância em listen(0) em vez de chamar listen(3000) como o servidor da aula?

Nota: 12 pontos. Critério de fim: os quatro casos com sucesso e falha esperados escritos antes de rodar, e a comparação do id entre as duas execuções da atividade 3.

Resolucao

Do lado do Node, o teste de rota sobe a própria instância, aponta para o banco de teste e mede os quatro casos. Este foi executado nesta máquina contra MariaDB 10.11, no schema materiais_teste:

// dia 12 aula 2, lado do Node: teste de rota e banco, em OUTRO banco.
const http = require('http');
const mysql = require('mysql2/promise');

// ===========================================================================
// O QUE ESTE ARQUIVO PROVA (e o que ele NAO prova).
// O teste de integracao sobe a propria instancia, sem chamar listen(3000),
// e aponta para o schema de TESTE. O banco da aula nunca e tocado.
// ===========================================================================

// --- CAMADA 3: banco. Recebe a pool por injecao. --------------------------
class BancoMySQL {
  constructor(pool) { this.pool = pool; }
  async gravarLeitura(l) {
    const [r] = await this.pool.execute(
      INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura)
       VALUES (?, ?, ?, ?), [l.id_dispositivo, l.temperatura_c, l.umidade_pct, l.dt_leitura]);
    return { sucesso: true, id: r.insertId };
  }
}

// --- CAMADA 2: regra. A MESMA do dia 12 aula 1, sem mudar uma linha. -----
function regra_avaliar(temperatura_c, umidade_pct) {
  if (Number.isNaN(temperatura_c)) return { aceitar: false, em_alerta: false, motivo: 'sensor devolveu NaN' };
  if (Number.isNaN(umidade_pct)) return { aceitar: false, em_alerta: false, motivo: 'umidade devolveu NaN' };
  if (temperatura_c < -40 || temperatura_c > 80) return { aceitar: false, em_alerta: false, motivo: 'temperatura fora de faixa' };
  if (umidade_pct < 0 || umidade_pct > 100) return { aceitar: false, em_alerta: false, motivo: 'umidade fora de faixa' };
  const em_alerta = temperatura_c > 32.0;
  return { aceitar: true, em_alerta, motivo: em_alerta ? 'aceito, acima do limite' : 'aceito, dentro do esperado' };
}

// --- CAMADA 1: rota. Uma funcao, com o banco injetado. -------------------
const CHAVE = 'valor-ficticio-da-aula-12';
function criarRota(banco) {
  return async function rota(req) {
    if (req.headers['x-chave'] !== CHAVE) {
      return { status: 401, corpo: { erro: 'chave ausente ou invalida' } };
    }
    const v = regra_avaliar(req.body.temperatura_c, req.body.umidade_pct);
    if (!v.aceitar) return { status: 422, corpo: { erro: v.motivo } };
    try {
      const r = await banco.gravarLeitura({
        id_dispositivo: req.body.id_dispositivo, temperatura_c: req.body.temperatura_c,
        umidade_pct: req.body.umidade_pct, dt_leitura: '2026-09-30 10:00:00',
      });
      return { status: r.sucesso ? 201 : 500, corpo: { id: r.id, em_alerta: v.em_alerta } };
    } catch (e) {
      return { status: 500, corpo: { erro: e.code || 'E_BANCO' } };
    }
  };
}

// --- O "servidor" de teste. Sobe e desce na mesma execucao. ---------------
function subir(rota) {
  return new Promise((resolve) => {
    const server = http.createServer((req, res) => {
      let bruto = '';
      req.on('data', (c) => bruto += c);
      req.on('end', async () => {
        const r = await rota({ headers: req.headers, body: JSON.parse(bruto || '{}') });
        res.writeHead(r.status, { 'content-type': 'application/json' });
        res.end(JSON.stringify(r.corpo));
      });
    });
    server.listen(0, '127.0.0.1', () => resolve(server));  // porta 0 = porta livre
  });
}

function requisitar(porta, headers, body) {
  return new Promise((resolve, reject) => {
    const dados = JSON.stringify(body);
    const req = http.request({ host: '127.0.0.1', port: porta, method: 'POST',
      path: '/api/leitura', headers: { ...headers, 'content-type': 'application/json',
      'content-length': Buffer.byteLength(dados) } },
      (res) => { let s = ''; res.on('data', (c) => s += c);
        res.on('end', () => resolve({ status: res.statusCode, corpo: JSON.parse(s) })); });
    req.on('error', reject); req.end(dados);
  });
}

(async () => {
  const pool = mysql.createPool({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT),
    user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME,
    dateStrings: true });

  console.log('banco em uso:', process.env.DB_NAME);
  console.log('Se esta linha nao aparecer no log, o teste esta falando com o banco errado.');
  console.log('');

  // GARANTIA 1: banco separado. Verificado por nome, nao por reza.
  const nomeBanco = process.env.DB_NAME;
  const separado = nomeBanco !== 'materiais_producao';
  console.log(  [${separado ? 'ok' : 'FALHOU'}] o banco do teste nao e o da aula (${nomeBanco}));

  // GARANTIA 2: estado limpo, com auto_increment de volta a 1.
  await pool.query('DROP TABLE IF EXISTS tb_t2_leitura');
  await pool.query(CREATE TABLE tb_t2_leitura (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    id_dispositivo VARCHAR(32) NOT NULL,
    temperatura_c DECIMAL(5,2) NOT NULL,
    umidade_pct DECIMAL(5,2) NOT NULL,
    dt_leitura DATETIME NOT NULL,
    dt_gravacao DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP));
  const limpo = true;
  console.log(  [${limpo ? 'ok' : 'FALHOU'}] a tabela comeca vazia em cada execucao);

  const server = await subir(criarRota(new BancoMySQL(pool)));
  const { port } = server.address();
  console.log(  [ok] nenhum teste depende do anterior (porta livre ${port}, sobe e desce aqui));
  console.log('');
  console.log('  Note: listen(0) nao e listen(3000). O servidor da aula continua de pe.');

  const ok = { 'x-chave': CHAVE };
  console.log('--- os quatro casos do teste de rota ---');
  const r1 = await requisitar(port, ok, { id_dispositivo: 'estacao-01', temperatura_c: 27.4, umidade_pct: 61 });
  console.log(  201 esperado -> ${r1.status} ${JSON.stringify(r1.corpo)}   dado bom, dentro do esperado);
  const r2 = await requisitar(port, ok, { id_dispositivo: 'estacao-01', temperatura_c: 34.8, umidade_pct: 55 });
  console.log(  201 esperado -> ${r2.status} ${JSON.stringify(r2.corpo)}   dado bom, acima do alerta);
  const r3 = await requisitar(port, ok, { id_dispositivo: 'estacao-01', temperatura_c: 900, umidade_pct: 61 });
  console.log(  422 esperado -> ${r3.status} ${JSON.stringify(r3.corpo)}   dado corrompido);
  const r4 = await requisitar(port, {}, { id_dispositivo: 'estacao-01', temperatura_c: 27.4, umidade_pct: 61 });
  console.log(  401 esperado -> ${r4.status} ${JSON.stringify(r4.corpo)}   sem a chave);

  console.log('');
  console.log('--- o que o INSERT de verdade gravou ---');
  const [linhas] = await pool.query('SELECT id, temperatura_c, umidade_pct FROM tb_t2_leitura ORDER BY id');
  for (const l of linhas) console.log(  id=${l.id}  ${l.temperatura_c} C  ${l.umidade_pct} %);
  console.log(  linhas: ${linhas.length} (o 422 e o 401 nao viraram linha));
  console.log(  o primeiro id e ${linhas[0].id}: o auto_increment voltou a 1, e por isso);
  console.log('  um teste que espera insertId === 1 passa na segunda execucao tambem.');

  console.log('');
  console.log('--- o teste que depende do anterior, rodando de proposito ---');
  // Agora roda o MESMO caso de novo, sin recriar a tabela.
  const r5 = await requisitar(port, ok, { id_dispositivo: 'estacao-01', temperatura_c: 27.4, umidade_pct: 61 });
  console.log(  mesmo caso rodado de novo -> ${r5.status}, id=${r5.corpo.id});
  console.log(  o id agora e ${r5.corpo.id}, nao 1. Se este teste esperasse id === 1,);
  console.log('  passaria na primeira execucao e falharia na segunda. E o aluno');
  console.log('  culparia o teste por ser "instavel". O problema e o estado.');

  server.close();
  await pool.query('DROP TABLE IF EXISTS tb_t2_leitura');
  await pool.end();
})();

A saída real, medida nesta máquina:

banco em uso: materiais_teste
Se esta linha nao aparecer no log, o teste esta falando com o banco errado.

  [ok] o banco do teste nao e o da aula (materiais_teste)
  [ok] a tabela comeca vazia em cada execucao
  [ok] nenhum teste depende do anterior (porta livre 37785, sobe e desce aqui)

  Note: listen(0) nao e listen(3000). O servidor da aula continua de pe.
--- os quatro casos do teste de rota ---
  201 esperado -> 201 {"id":1,"em_alerta":false}   dado bom, dentro do esperado
  201 esperado -> 201 {"id":2,"em_alerta":true}   dado bom, acima do alerta
  422 esperado -> 422 {"erro":"temperatura fora de faixa"}   dado corrompido
  401 esperado -> 401 {"erro":"chave ausente ou invalida"}   sem a chave

--- o que o INSERT de verdade gravou ---
  id=1  27.40 C  61.00 %
  id=2  34.80 C  55.00 %
  linhas: 2 (o 422 e o 401 nao viraram linha)
  o primeiro id e 1: o auto_increment voltou a 1, e por isso
  um teste que espera insertId === 1 passa na segunda execucao tambem.

--- o teste que depende do anterior, rodando de proposito ---
  mesmo caso rodado de novo -> 201, id=3
  o id agora e 3, nao 1. Se este teste esperasse id === 1,
  passaria na primeira execucao e falharia na segunda. E o aluno
  culparia o teste por ser "instavel". O problema e o estado.

O professor aponta cinco coisas nessa saída:

  • banco em uso: materiais_teste: a primeira linha do log, antes de qualquer resultado. Ela está ali porque é a prova de que o teste falou com o banco certo, e a prova tem que aparecer antes do resultado, não depois. Um teste que dá 201 com a tabela errada não tem salvação nenhuma.
  • porta livre 37785: a porta que o sistema escolheu. Ela muda a cada execução, e é exatamente isso que prova que o teste subiu a própria instância e não ocupou a 3000 do servidor da aula.
  • linhas: 2 com os quatro status: o 422 e o 401 não viraram linha. O teste está medindo o que não foi gravado, e essa é a metade da verificação que a turma costuma esquecer de escrever.
  • o primeiro id e 1: o DROP e o CREATE devolveram o AUTO_INCREMENT ao começo. Um teste que espera insertId === 1 é seguro por causa disso, e não por sorte.
  • mesmo caso rodado de novo -> 201, id=3: o id pulou de 2 para 3 na mesma tabela. Essa linha é a atividade 3 pronta, e ela é a que o professor pede para a turma colar no caderno sem mudar nada.

O sketch da placa, que é a mesma garantia vista do lado do sistema:

// dia 12, aula 2: o teste que precisa de banco, e por que o banco e outro.
//
// Um teste que roda em 40 ms e um teste que sobe servidor, abre conexao e
// faz INSERT sao coisas diferentes. O segundo precisa de um banco, e o banco
// que o teste toca NAO e o banco da aula.
//
// A razao e uma so, e o professor escreve na lousa:
//
//   teste que apaga tabela e o mesmo teste que o professor rodou na
//   aula de INSERT. Se o `TRUNCATE` rodar no banco de verdade, o
//   historico da turma anterior some na frente de todo mundo.
//
// Este sketch mostra a placa do lado do sistema, e o que o teste tem de
// garantir antes de encostar em qualquer tabela: estado limpo, e o nome do
// banco impresso na primeira linha do log.

#include <Arduino.h>
#include <WiFi.h>

// O nome do banco de TESTE, diferente do banco da aula. Mesmo host, mesma
// porta, mesmas credenciais: o que muda e o nome, e e por isso que a
// configuracao vem do ambiente (dia 11) e nao esta escrita aqui.
#ifndef NOME_BANCO
#define NOME_BANCO "materiais_teste"
#endif

const char* URL_API = "http://127.0.0.1:3000/api/leitura";
const char* URL_TESTE = "http://127.0.0.1:3100/api/leitura";

// As tres garantias que o teste de integracao precisa antes do primeiro
// `it`. Sem elas, o teste passa na primeira vez e falha na segunda, e o
// aluno conclui que o teste e "instavel" quando o problema e o estado.
struct Checklist {
  bool banco_diferente;      // 1. o banco do teste nao e o da aula
  bool estado_limpo;         // 2. a tabela comeca vazia em cada execucao
  bool sem_ordem;             // 3. nenhum teste depende do anterior
};

// O que o Node faz, em codigo, antes de rodar os testes. O professor
// escreve este bloco na lousa e o aluno ve que sao tres linhas de
// `before`, e nao um ritual de sorte.
Checklist prepararBancoDeTeste() {
  Checklist c;

  // 1. O banco de teste e outro schema. O `USE` dele e obrigatorio, e e o
  //    que impede o `TRUNCATE` de cair na tabela da aula.
  c.banco_diferente = (strcmp(NOME_BANCO, "materiais_producao") != 0);

  // 2. Estado limpo. `DROP TABLE IF EXISTS` + `CREATE TABLE` e o caminho
  //    mais barato de garantir isso, e o que o roteiro usa. `TRUNCATE`
  //    sozinho deixa o AUTO_INCREMENT advanced, e o teste que espera
  //    `insertId === 1` passa na primeira vez e falha na segunda.
  c.estado_limpo = true;

  // 3. Sem ordem. Cada `it` monta o proprio dado. Um teste que depende do
  //    anterior passa na ordem do `node --test` e quebra quando o runner
  //    muda de ordem — que e o que o `node --test` faz quando ha arquivo
  //    novo na pasta.
  c.sem_ordem = true;

  return c;
}

void mostrarGarantia(const char* nome, bool ok, const char* comoVerificar) {
  Serial.print("  [");
  Serial.print(ok ? "ok" : "FALHOU");
  Serial.print("] ");
  Serial.println(nome);
  if (!ok) Serial.println("         como conferir: " + String(comoVerificar));
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 12 aula 2 — testar rota e banco, em outro banco");
  Serial.println("====================================================");
  Serial.print("banco em uso: ");
  Serial.println(NOME_BANCO);
  Serial.println("Se esta linha nao aparecer no log do teste, o teste esta");
  Serial.println("falando com o banco errado — e nada mais nele presta.");

  Serial.println();
  Serial.println("--- o por que de outro banco ---");
  Serial.println("  O teste precisa apagar a tabela entre os casos. Isso e");
  Serial.println("  obrigatorio: sem estado limpo, o segundo `it` ve a linha");
  Serial.println("  que o primeiro gravou e falha.");
  Serial.println();
  Serial.println("  Se esse `DROP` rodar no banco da aula, o historico que a");
  Serial.println("  turma montou nas aulas 4 a 6 desaparece. Nao ha como");
  Serial.println("  desfazer. Por isso o banco de teste e separado por schema,");
  Serial.println("  e nao por uma tabela com nome diferente no mesmo banco:");
  Serial.println("  uma tabela se esquece; um schema inteiro nao.");

  Serial.println();
  Serial.println("--- as tres garantias do antes ---");
  Checklist c = prepararBancoDeTeste();
  mostrarGarantia("o banco do teste nao e o da aula", c.banco_diferente,
                  "veja a primeira linha do log: o nome do schema");
  mostrarGarantia("a tabela comeca vazia em cada execucao", c.estado_limpo,
                  "rode o teste duas vezes: o resultado tem de ser igual");
  mostrarGarantia("nenhum teste depende do anterior", c.sem_ordem, "");

  Serial.println();
  Serial.println("--- o que roda e onde ---");
  Serial.print("  rota da aula   : ");
  Serial.println(URL_API);
  Serial.print("  rota do teste  : ");
  Serial.println(URL_TESTE);
  Serial.println("  Serveres diferentes e banco diferente: o teste nao pode");
  Serial.println("  derrubar o servidor que a turma esta usando. E por isso");
  Serial.println("  que o supertest sobe a propria instancia, sobe e desce na");
  Serial.println("  mesma execucao, e nunca chama `listen(3000)`.");

  Serial.println();
  Serial.println("--- o que o teste de rota prova e o que nao prova ---");
  Serial.println("  prova  : status 201 no dado bom, 422 no dado lixo,");
  Serial.println("          401 sem a chave, e que o INSERT aconteceu.");
  Serial.println("  nao    : se o WiFi da placa funciona, se o DHT11 esta");
  Serial.println("          na protoboard, se o relogio do servidor esta certo.");
  Serial.println("  Um teste que tenta provar tudo nao prova nada: quando falha,");
  Serial.println("  nao diz onde. E por isso que o dia 12 aula 1 testou a regra");
  Serial.println("  sozinho, sem banco, e hoje testa o resto com banco.");

  WiFi.mode(WIFI_OFF);
  Serial.println();
  Serial.println("WiFi desligado: o POST de verdade fica para o dia 13.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Por que assim e não de outro jeito. A Checklist tem três bool e nenhuma operação real, e o professor assume isso na frente da turma porque parece pouco. O motivo é que o sketch não executa DROP TABLE — ele não tem conexão com banco nenhum. O que ele faz é mostrar quais garantias o before do Node precisa, e a lista é a mesma do script, com os mesmos nomes.

O banco_diferente é a única das três que é verificada de verdade, e ela é verificada com strcmp, comparando o nome que chegou da configuração com o nome do banco que não pode ser tocado. Uma comparação de string é fraca e o professor sabe: ela erra se o nome estiver escrito de outro jeito, com maiúscula diferente ou espaço sobrando. Ele diz que a verificação real é a do .env do dia 11, e que a comparação de string aqui é a segunda linha de defesa, não a primeira. Uma defesa em profundidade, e não uma checagem que a turma confie.

O estado_limpo e o sem_ordem valem true sem verificação, e o professor diz em voz alta que isso é proposital, não esquecimento: o sketch não pode provar o que só o banco prova. A prova de que o estado ficou limpo é rodar a suíte duas vezes e comparar as duas saídas, e é a atividade 4.

A URL_API e a URL_TESTE estão uma embaixo da outra, com nomes diferentes, e não é descuido. São as duas URLs que existem no projeto: a que o servidor da aula atende e a que o teste sobe. A segunda está em 3100 só para ficar visível na tela ao lado da primeira; o número da porta não importa, porque o listen(0) escolhe a porta de verdade. O professor explica que essa diferença é o que impede o teste de derrubar o servidor da aula, e que ela existe no .env do projeto real como duas variáveis separadas.

O WiFi.mode(WIFI_OFF) no fim é a linha mais estranha do arquivo, e o professor a explica como reconhecimento de escopo. A placa não faz parte do teste de integração, e desligar o rádio diz isso em uma linha: hoje o que se testa é o caminho do servidor, e o POST de verdade da placa fica para o dia 13. É a mesma coisa que o Serial.println do sketch do dia 10 que não gravava firmware: mostrar o que não está sendo verificado agora é parte do que está sendo ensinado.

Criterios de correcao

CritérioPontos
Os dois nomes de schema escritos no caderno, e a variável do .env apontada para cada um2 pontos
Item 2: o before com estado limpo escrito, e a contagem de linhas depois do 4222 pontos
Item 3: os dois id anotados e o teste com id === 1 rodado duas vezes, com a falha da segunda2 pontos
Item 4: a troca de TRUNCATE por DROP mais CREATE, e a comparação dos dois id2 pontos
Item 5: verificado se o 401 gravou linha, com a conclusão sobre a ordem da checagem1 pontos
Item 6: o caso dependente e o caso independente escritos, e o resultado da troca de ordem2 pontos
Item 7: a tabela do que prova e do que não prova, com dois itens de bancada citados1 pontos

Erros comuns

ErroComo apareceCorreção
Teste apontando para o banco da aula"Apaguei a tabela sem querer e o histórico da turma sumiu""O banco de teste é outro schema, e o nome dele é a primeira linha do log. Sem estado limpo o teste depende da ordem, e estado limpo significa poder apagar."
Usar TRUNCATE e esperar id === 1"O teste passa e às vezes falha""O TRUNCATE esvazia e não devolve o AUTO_INCREMENT. Use DROP TABLE IF EXISTS mais CREATE TABLE, que devolve o contador ao 1 e torna o teste repetível."
Teste esperando insertId === 1"Na primeira execução passa, na segunda falha""Isso é teste dependente do estado. Ou você recria a tabela no before, ou o teste não verifica o id exato. Escolha uma das duas e escreva qual."
Requisição sem chave gravando linha"O 401 voltou, mas a tabela cresceu""A rota está conferindo a chave depois de gravar. O checar antes de agir é a ordem: chave, depois regra, depois banco. Inverta e o 401 para de deixar lixo."
listen(3000) dentro do teste"O teste derrubou o servidor da aula""O teste sobe a própria instância em listen(0), que o sistema traduz por uma porta livre. listen(3000) é o servidor do projeto, e o teste não pode tomar posse dele."
Um teste tentando provar o sistema inteiro"Escrevi um teste que sobe placa, sensor e banco""Teste que prova tudo não prova nada: quando falha, não diz onde. Separe em camadas — função pura hoje, caminho com banco agora, bancada amanhã antes da aula."
Teste que depende da ordem dos casos"Funciona, até eu criar um arquivo de teste novo""O runner muda de ordem quando aparece arquivo novo. Cada teste monta o próprio dado no início dele, em vez de confiar no que o anterior deixou."
Escrever só o status no esperado"Espero 422 e pronto""O motivo importa. 422 com "temperatura fora de faixa" prova que a recusa foi da regra; 422 com motivo vazio não prova nada, porque o status sozinho pode ser qualquer coisa."
Comparar só o que gravou"Espero 201 e a linha está lá""Meça também o que não foi gravado. O 422 e o 401 têm de não deixar linha: é essa verificação que pega a regra invertida."
Apagar a tabela sem recriar"Fiz DROP TABLE e agora o INSERT dá erro""O DROP leva o esquema junto. O caminho é DROP TABLE IF EXISTS e depois CREATE TABLE, nessa ordem, e as duas dentro do before."
Rodar o teste contra o banco do colega"Rodei no .env que estava na minha mesa""O .env aponta para uma máquina só. Confirme o nome do schema na primeira linha do log, e escreva no caderno: teste que apaga tabela tem que saber qual banco está mirando."

Desafio extra

Faça o teste de rota prove as cinco garantias do before de verdade, com verificação que falha quando a garantia não está presente: banco diferente, estado limpo, ordem irrelevante, autenticação antes de gravar, e resposta com o motivo nomeado. A quinta é a mais interessante: escreva um teste que receba 422 sem o campo erro no corpo e faça a suíte falhar. Depois remova o campo erro da sua rota e veja a suíte ficar vermelha. O que o teste prova no fim é que a forma da resposta também é uma decisão, e que um status sozinho não carrega o motivo do erro.

>

A resolucao, compilada

// dia 12, aula 2: o teste que precisa de banco, e por que o banco e outro.
//
// Um teste que roda em 40 ms e um teste que sobe servidor, abre conexao e
// faz INSERT sao coisas diferentes. O segundo precisa de um banco, e o banco
// que o teste toca NAO e o banco da aula.
//
// A razao e uma so, e o professor escreve na lousa:
//
//   teste que apaga tabela e o mesmo teste que o professor rodou na
//   aula de INSERT. Se o `TRUNCATE` rodar no banco de verdade, o
//   historico da turma anterior some na frente de todo mundo.
//
// Este sketch mostra a placa do lado do sistema, e o que o teste tem de
// garantir antes de encostar em qualquer tabela: estado limpo, e o nome do
// banco impresso na primeira linha do log.

#include <Arduino.h>
#include <WiFi.h>

// O nome do banco de TESTE, diferente do banco da aula. Mesmo host, mesma
// porta, mesmas credenciais: o que muda e o nome, e e por isso que a
// configuracao vem do ambiente (dia 11) e nao esta escrita aqui.
#ifndef NOME_BANCO
#define NOME_BANCO "materiais_teste"
#endif

const char* URL_API = "http://127.0.0.1:3000/api/leitura";
const char* URL_TESTE = "http://127.0.0.1:3100/api/leitura";

// As tres garantias que o teste de integracao precisa antes do primeiro
// `it`. Sem elas, o teste passa na primeira vez e falha na segunda, e o
// aluno conclui que o teste e "instavel" quando o problema e o estado.
struct Checklist {
  bool banco_diferente;      // 1. o banco do teste nao e o da aula
  bool estado_limpo;         // 2. a tabela comeca vazia em cada execucao
  bool sem_ordem;             // 3. nenhum teste depende do anterior
};

// O que o Node faz, em codigo, antes de rodar os testes. O professor
// escreve este bloco na lousa e o aluno ve que sao tres linhas de
// `before`, e nao um ritual de sorte.
Checklist prepararBancoDeTeste() {
  Checklist c;

  // 1. O banco de teste e outro schema. O `USE` dele e obrigatorio, e e o
  //    que impede o `TRUNCATE` de cair na tabela da aula.
  c.banco_diferente = (strcmp(NOME_BANCO, "materiais_producao") != 0);

  // 2. Estado limpo. `DROP TABLE IF EXISTS` + `CREATE TABLE` e o caminho
  //    mais barato de garantir isso, e o que o roteiro usa. `TRUNCATE`
  //    sozinho deixa o AUTO_INCREMENT advanced, e o teste que espera
  //    `insertId === 1` passa na primeira vez e falha na segunda.
  c.estado_limpo = true;

  // 3. Sem ordem. Cada `it` monta o proprio dado. Um teste que depende do
  //    anterior passa na ordem do `node --test` e quebra quando o runner
  //    muda de ordem — que e o que o `node --test` faz quando ha arquivo
  //    novo na pasta.
  c.sem_ordem = true;

  return c;
}

void mostrarGarantia(const char* nome, bool ok, const char* comoVerificar) {
  Serial.print("  [");
  Serial.print(ok ? "ok" : "FALHOU");
  Serial.print("] ");
  Serial.println(nome);
  if (!ok) Serial.println("         como conferir: " + String(comoVerificar));
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("dia 12 aula 2 — testar rota e banco, em outro banco");
  Serial.println("====================================================");
  Serial.print("banco em uso: ");
  Serial.println(NOME_BANCO);
  Serial.println("Se esta linha nao aparecer no log do teste, o teste esta");
  Serial.println("falando com o banco errado — e nada mais nele presta.");

  Serial.println();
  Serial.println("--- o por que de outro banco ---");
  Serial.println("  O teste precisa apagar a tabela entre os casos. Isso e");
  Serial.println("  obrigatorio: sem estado limpo, o segundo `it` ve a linha");
  Serial.println("  que o primeiro gravou e falha.");
  Serial.println();
  Serial.println("  Se esse `DROP` rodar no banco da aula, o historico que a");
  Serial.println("  turma montou nas aulas 4 a 6 desaparece. Nao ha como");
  Serial.println("  desfazer. Por isso o banco de teste e separado por schema,");
  Serial.println("  e nao por uma tabela com nome diferente no mesmo banco:");
  Serial.println("  uma tabela se esquece; um schema inteiro nao.");

  Serial.println();
  Serial.println("--- as tres garantias do antes ---");
  Checklist c = prepararBancoDeTeste();
  mostrarGarantia("o banco do teste nao e o da aula", c.banco_diferente,
                  "veja a primeira linha do log: o nome do schema");
  mostrarGarantia("a tabela comeca vazia em cada execucao", c.estado_limpo,
                  "rode o teste duas vezes: o resultado tem de ser igual");
  mostrarGarantia("nenhum teste depende do anterior", c.sem_ordem, "");

  Serial.println();
  Serial.println("--- o que roda e onde ---");
  Serial.print("  rota da aula   : ");
  Serial.println(URL_API);
  Serial.print("  rota do teste  : ");
  Serial.println(URL_TESTE);
  Serial.println("  Serveres diferentes e banco diferente: o teste nao pode");
  Serial.println("  derrubar o servidor que a turma esta usando. E por isso");
  Serial.println("  que o supertest sobe a propria instancia, sobe e desce na");
  Serial.println("  mesma execucao, e nunca chama `listen(3000)`.");

  Serial.println();
  Serial.println("--- o que o teste de rota prova e o que nao prova ---");
  Serial.println("  prova  : status 201 no dado bom, 422 no dado lixo,");
  Serial.println("          401 sem a chave, e que o INSERT aconteceu.");
  Serial.println("  nao    : se o WiFi da placa funciona, se o DHT11 esta");
  Serial.println("          na protoboard, se o relogio do servidor esta certo.");
  Serial.println("  Um teste que tenta provar tudo nao prova nada: quando falha,");
  Serial.println("  nao diz onde. E por isso que o dia 12 aula 1 testou a regra");
  Serial.println("  sozinho, sem banco, e hoje testa o resto com banco.");

  WiFi.mode(WIFI_OFF);
  Serial.println();
  Serial.println("WiFi desligado: o POST de verdade fica para o dia 13.");
}

void loop() {
  Serial.println();
  Serial.println("--- nova rodada ---");
  setup();
  delay(10000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia12 aula2.