Teste — Arduino e IoT — semana 12 do 2o trimestre
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,iteassert. - 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
DHT11por dupla, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3 - 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 chama | Por que quebra a pureza | Como o teste falha |
|---|---|---|
sensor.readTemperature() | o resultado depende do fio, do 3V3 e da bancada | passa na sua bancada, falha na do professor |
millis() ou now() | o resultado depende de quando rodou | passa hoje, falha amanhã |
| uma conexão de banco | o resultado depende do que tem gravado | passa com a tabela vazia, falha com ela cheia |
console.log | grava fora, e o teste não limpa | passa e deixa lixo |
Math.random() | resultado diferente a cada chamada | falha 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ça | O que é | Para que serve |
|---|---|---|
test(nome, funcao) | o caso | nomeia a verificação e a executa |
describe(nome, funcao) | o grupo | junta casos de uma mesma regra |
assert.equal(obtido, esperado) | a verificação | compara e interrompe se divergir |
assert.ok(condicao) | a verificação booleana | para 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:
DHT11noGPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3.- Cabo USB conectado, monitor serial aberto em 115200.
- 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.
- 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.
- 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?
- 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. - 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? - Escreva um teste que prove que
NaNe 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. - Grave na placa o sketch da resolução e leia a saída. Compare, caso a caso, o que o
Serialmostrou com o que onode --testmostrou na atividade 2. Algum caso discordou? O que isso diz sobre o sensor? - Em uma frase: por que essa suíte pode rodar em todo
pushe 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 1detests 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.433793msno caso que falhou: o tempo é irrelevante para a correção e é o argumento principal para opush. 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ério | Pontos |
|---|---|
| Função pura do projeto identificada, com a linha do código marcada | 2 pontos |
| Os oito casos escritos com nome, entrada e resultado esperado, na tabela do caderno | 3 pontos |
| Item 3: a linha de resumo colada, com número de testes, aprovados e tempo em milissegundos | 1 pontos |
| Item 4: o operador trocado, a suíte rodada e a linha do erro vermelha colada | 2 pontos |
| Caso de borda do projeto escrito, com a entrada no limite e a decisão tomada | 2 pontos |
Teste que distingue NaN de zero escrito, e a explicação do que o primeiro deixaria passar | 1 pontos |
Item 7: comparação caso a caso entre o Serial e o node --test, com o caso que discordou | 1 pontos |
Erros comuns
| Erro | Como aparece | Correçã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 TABLEeCREATE TABLEvale mais queTRUNCATEaqui. - 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,
mysql2e 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:
| Maneira | O que é | Por que não serve |
|---|---|---|
| outra tabela no mesmo banco | tb_t2_teste ao lado de tb_t2_leitura | uma tabela se esquece; um TRUNCATE sem WHERE erra a tabela e apaga a da aula |
| outro schema | materiais_teste ao lado de materiais_producao | o 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:
| Prova | Não prova |
|---|---|
| o status de cada caso | se o WiFi da placa funciona |
que o INSERT aconteceu | se o DHT11 está na protoboard |
que o 422 veio da regra, com motivo | se o relógio do servidor está certo |
que o 401 acontece antes de gravar | se a placa está mandando no intervalo certo |
que o JSON de saída tem os campos | que 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.
- Escreva no caderno o nome do schema do seu banco de teste e o do banco da aula. Marque no seu
.envqual variável aponta para cada um, e diga qual delas onpm testdeve ler. - Escreva o
beforeda 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. O422gravou alguma linha? - Rode o mesmo caso de dado bom duas vezes sem recriar a tabela, e anote o
iddevolvido em cada uma. Agora escreva um teste que exijaid === 1e rode a suíte duas vezes seguidas. Ele falha na segunda? O que oAUTO_INCREMENTtem a ver com isso? - Troque o
TRUNCATEdo seubeforeporDROP TABLE IF EXISTSmaisCREATE TABLEe rode a suíte duas vezes. Oidvoltou a 1 nas duas? O que a diferença entreTRUNCATEeDROPte diz sobre estado limpo? - 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.
- 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?
- 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.
- Em uma frase: por que o teste sobe a própria instância em
listen(0)em vez de chamarlisten(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á201com 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 a3000do servidor da aula.linhas: 2com os quatro status: o422e o401nã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: oDROPe oCREATEdevolveram oAUTO_INCREMENTao começo. Um teste que esperainsertId === 1é seguro por causa disso, e não por sorte.mesmo caso rodado de novo -> 201, id=3: oidpulou 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ério | Pontos |
|---|---|
Os dois nomes de schema escritos no caderno, e a variável do .env apontada para cada um | 2 pontos |
Item 2: o before com estado limpo escrito, e a contagem de linhas depois do 422 | 2 pontos |
Item 3: os dois id anotados e o teste com id === 1 rodado duas vezes, com a falha da segunda | 2 pontos |
Item 4: a troca de TRUNCATE por DROP mais CREATE, e a comparação dos dois id | 2 pontos |
Item 5: verificado se o 401 gravou linha, com a conclusão sobre a ordem da checagem | 1 pontos |
| Item 6: o caso dependente e o caso independente escritos, e o resultado da troca de ordem | 2 pontos |
| Item 7: a tabela do que prova e do que não prova, com dois itens de bancada citados | 1 pontos |
Erros comuns
| Erro | Como aparece | Correçã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.
