Teste a fundo — Arduino e IoT — semana 5 do 3o trimestre
Semana 5 de 15· 3o trimestre · 05/09 a 10/12
Teste a fundo
O teste deixa de ser o que roda verde e vira o que impede a regressao.
Aula 1 — Teste unitario com regra e com maquina de estados
Objetivos
- Escrever o teste unitário de uma regra de negócio que roda sem banco, sem rede e sem placa, e medir quanto tempo leva.
- Cobrir a regra com os três tipos de caso — normal, limite e de erro — e escrever a tabela de casos que os lista.
- Testar a máquina de estados percorrendo todas as transições, incluindo a transição impossível.
- Escrever o teste que falha primeiro, e explicar por que essa é a ordem certa e não a ordem preguiçosa.
- Construir o mock, o dublê e o repository falso, e dizer o que cada um resolve e o que ele esconde.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto e a biblioteca de teste instalada
- Folha de papel por dupla, com a tabela de casos da regra
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal com o teste rodando e o relatório na tela
Conceitos
Teste unitário é o que roda sem o mundo
Um teste unitário testa uma unidade de código isolada: uma função, uma regra, uma transição. A palavra que importa é unitário, e ela define tudo o que o resto da técnica precisa ser.
O critério de um teste unitário, e a resposta é que ele tem de conseguir testar sem banco, é uma pergunta que o professor aplica a qualquer teste que a turma escrever: esse teste precisa de banco? precisa de rede? precisa de placa? Se a resposta é sim para qualquer uma das três, o teste não é unitário — é de integração, e ele tem as consequências de um teste de integração: é lento, é frágil e não diz onde está o defeito.
O caso normal é o caminho feliz, e ele é o mais fácil de escrever e o menos útil sozinho. Uma regra com cinco caminhos e um teste do caminho feliz e a cobertura parece inteira, porque a linha do if foi executada. Cobertura de linha não é cobertura de regra, e essa é a diferença que a tabela de casos corrige.
O caso limite é o que pega o defeito que sobrevive a meses. A regra do horário da aula do dia 1 tem dois limites: 6 entra e 22 não entra. O caso normal testa 10h e passa; o caso limite testa 6h, 5h59, 22h, 22h1 e encontra o >= escrito como >. O defeito de borda é o que não aparece na demonstração e aparece no primeiro relatório de campo às seis da manhã.
O caso de erro é o que a regra recusa, e aqui está a conexão com o dia 1: cada recusa da regra é um caso de teste com um código esperado. A tabela de casos da regra do horário tem seis linhas, e quatro delas são recusas.
Escrever o teste antes do código é a prática do teste que falha primeiro que o professor mais exige, e o nome vem da verdade que ele explica: quando o teste é escrito antes do código, ele falha primeiro, e a falha é a prova de que o teste está medindo alguma coisa. Um teste que passa assim que é escrito não está medindo: está repetindo o que o código faz. A ordem é escrever o teste, ver reprovar, escrever o mínimo que faz passar, e refatorar com a suíte verde.
O que a suíte verde não garante, e o professor escreve na lousa: ela garante que o que estava certo continua certo. Ela não garante que o que está errado foi notado — e o item 6 da atividade é exatamente esse teste, o que a suíte atual não vê.
A tabela de casos, que é o que o teste realmente é
A lista de casos de teste é a tabela de casos: a lista de entradas e resultados esperados de uma regra, escrita antes do código. Ela não é documentação: é o teste, escrito em forma de tabela para que o programador e quem define o requisito possam ler a mesma coisa.
A tabela do projeto, para a regra do horário com o limite de faixa, é esta, e o professor a preenche com a turma:
| Caso | Entrada | Resultado esperado | Por que esse caso existe |
|---|---|---|---|
| normal | hora 10, leitura 48 | 201, gravou | o caminho que acontece todo dia |
| limite inferior | hora 6, leitura 48 | 201, gravou | a primeira hora que entra |
| limite inferior menos um | hora 5, leitura 48 | 422, recusada | pega o > escrito como >= |
| limite superior | hora 22, leitura 48 | 422, recusada | a primeira hora que não entra |
| faixa do sensor | hora 10, leitura -1 | 503, fonte falhou | o -1 não é zero |
| dado fora de faixa alta | hora 10, leitura 150 | 503, fonte falhou | o teto da faixa confiavel |
A coluna da direita é a que a maioria dos projetos não tem, e ela é a que transforma a tabela em defesa. Sem ela, ninguém sabe por que o caso existe e alguém o apaga na próxima revisão por parecer redundante. Com ela, apagar o caso exige responder à pergunta.
A tabela vira código de forma direta: cada linha é uma chamada com a entrada e a comparação com o esperado. E o critério de fim é objetivo: todos os casos da tabela estão no teste e nenhum caso falha. Se um caso da tabela não está no teste, a tabela está mentindo sobre o sistema.
A máquina de estados testada, as transições e a que não existe
A maquina de estados testada — com acento na escrita, como no eixo — é o caso de teste mais mecânico e mais poderoso do curso, e a razão é a estrutura: a tabela de transições é uma lista, e lista se percorre inteira.
O teste que percorre todas as transições toma cada linha da tabela, coloca a máquina no estado de origem, aplica o evento e compara o estado de destino com o que a linha diz. Feito isso uma vez para cada linha, o teste prova duas coisas: que toda transição declarada funciona, e — mais importante — que a tabela não tem linha sobrando.
A transição impossível testada é a parte que quase todo mundo pula, e é a que encontra os defeitos de estrutura. O teste toma cada par de estado e evento que não está na tabela e confirma que a máquina recusa. São cinco estados e cinco eventos: vinte e cinco pares, sete na tabela, dezoito fora. Os dezoito precisam recusar, e o teste que os percorre é o que impede a tabela de crescer por engano com uma linha que ninguém queria.
O efeito prático é o que a aula 1 do dia 2 descobre na prática: com a tabela de duas colunas e o destino lido errado, todas as transições da tabela "passam" no teste, porque o destino que o código produz coincide com o destino que o código espera — os dois vêm da mesma coluna. Um teste que só compara o código consigo mesmo não pega esse defeito. O teste que pega é o que compara contra a expectativa escrita por fora da tabela, e é por isso que a tabela de casos da aula 1 precisa existir.
O que o teste da máquina de estados mede, e que o professor mede na frente da turma, é a contagem: quantas transições aceitas, quantas recusadas, e se os dois números batem com a contagem da tabela. Com a tabela de sete linhas e o roteiro de nove eventos, o número esperado é sete aceitas e duas recusadas. Com o defeito da coluna, o número medido é três e cinco, e a diferença entre as duas contagens é o defeito inteiro.
Mock, dublê e substituto: três palavras para a mesma ideia
O mock é um substituto que verifica como foi chamado. O dublê é um substituto que devolve um valor combinado. O substituto é o nome geral, e repository falso é o nome que o projeto usa — é um dublê, não um mock, porque devolve dado em vez de verificar chamada.
A distinção não é acadêmica: ela decide o que o teste consegue verificar. Um dublê que devolve o que o teste mandou permite verificar o resultado da regra: "com o repository devolvendo nome em uso, a regra devolve 409". Um mock que só registra a chamada permite verificar o caminho: "com o repository recusando, a regra não chamou gravar". Para a regra das duas fontes, o que a turma precisa é o mock com contagem, que responde as duas perguntas ao mesmo tempo: devolve o combinado e soma um.
A razão de o teste unitário poder existir está aqui. Sem substituto, testar a regra exige um banco com dados que o teste monta, e o teste herda tudo do banco: a lentidão, a conexão, o estado deixado pela execução anterior. Com substituto, o teste tem os dados no próprio arquivo, e o único estado é o que o teste criou. É a razão de a camada do dia 4 existir, e o professor fecha o ciclo em voz alta: a separação em camadas não é para o código ficar bonito, é para o teste ser possível.
O que o substituto esconde é a parte que o professor pede para a turma escrever. Um repository falso que sempre devolve 201 vai fazer a regra passar em todos os casos de recusa, porque a recusa nunca acontece. O que protege contra isso é o caso de erro da tabela, com o substituto devolvendo justamente a falha. E o que protege do outro lado — o repository que não devolve o que a regra precisa — é o caso em que o teste manda "fonte sem resposta" e a regra precisa conviver com isso.
O limite do que o teste unitário garante é o que fecha a aula. Ele garante que a regra está certa. Ele não garante que a consulta existe, que a tabela tem a coluna, que a rota chama o método certo, que o painel mostra o retorno. Esses são os defeitos do teste de ponta a ponta da aula 2, e a turma precisa saber a linha entre os dois: o teste unitário responde "a decisão está certa?", e nada além disso.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Banco desligado e rede desligada: o teste da atividade tem que passar assim mesmo.
- Folha de papel com a tabela de casos da regra, com a coluna "por que esse caso existe".
- Escreva a tabela de casos da sua regra de horário, com as seis linhas: normal, os três limites e os dois de erro. Para cada linha, escreva o resultado esperado com o código do dia 1. Sem o código, o caso não é verificável.
- Transforme a tabela em teste, um caso por linha. Rode com o banco desligado e anote o tempo em milissegundos. Se precisar ligar o banco para rodar, escreva qual chamada fez isso e por quê.
- Escreva o teste que falha primeiro: escolha um caso da tabela que o seu código hoje não atende, escreva o teste e rode antes de mexer no código. Cole a mensagem de reprovação. Ela cita o nome da função e a linha?
- Escreva o repository falso da sua regra: os métodos que a regra chama, o que cada um devolve e a contagem de chamadas. Quantas linhas tem? Escreva a asserção que confere a contagem do caminho de recusa.
- Escreva o teste da transição impossível da sua máquina de estados: um caso que aplica um evento proibido e confirma que o estado não mudou. Ele falha hoje? Se falha, a linha do código que causa isso é a que do
ifou a da tabela? - Escreva um teste que a suíte atual não pega, e rode para ver que ele reprova. Esse é o teste que o professor chama de buraco: existe um caso em que o sistema está errado e nada acusa. Escreva em uma frase o que ele encontrou.
- Grave o sketch da resolução e rode por um minuto. Copie no caderno as três contagens que ele imprime: transições da tabela, transições aceitas e transições recusadas. As três batem com o que você escreveu no papel?
- Meça o tempo do teste duas vezes: com o banco ligado e desligado. Escreva os dois números e calcule a diferença. Baseado nesse número, escreva quantas vezes por dia o seu time roda a suíte inteira.
Nota: 12 pontos. Critério de fim: a tabela de casos com os seis casos e os códigos esperados, o teste do item 3 reprovando com a mensagem de reprovação colada, e as três contagens do sketch copiadas e conferidas com o papel.
Resolucao
O sketch da aula está em codigo/t3/dia05/aula1.ino.
#include <Arduino.h> // =========================================================================== // dia 5, aula 1: teste unitario com regra e com maquina de estados. // // Nenhuma biblioteca de teste entra aqui: a placa nao tem runner de teste, e // o que ela tem e um CONTADOR. E a mesma ideia do teste, feita com o que o // alvo permite: cada caso e uma funcao que devolve 0 ou o numero do caso que // falhou, e o `loop` soma. A tabela de casos do enunciado vira a tabela // `CASOS` abaixo, linha por linha. // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 4000; // A REGRA, isolada. Nao imprime, nao chama Serial, nao conhece LED: e por isso // que o teste dela roda sem placa. Esta e a mesma regra do dia 1. const int HORA_INICIO = 6; const int HORA_FIM = 22; const int LEITURA_MINIMA = 10; const int LEITURA_MAXIMA = 90; int avaliarLeitura(int hora, int leitura) { if (leitura < LEITURA_MINIMA || leitura > LEITURA_MAXIMA) return 503; if (hora < HORA_INICIO || hora >= HORA_FIM) return 422; return 201; } // --------------------------------------------------------------------------- // A TABELA DE CASOS. Cada linha e um caso, e o comentario ao lado e a coluna // "por que esse caso existe" que impede alguem de apagar a linha na revisao. // --------------------------------------------------------------------------- struct Caso { const char* nome; int hora; int leitura; int esperado; const char* porque; }; const Caso CASOS[] = { {"normal", 10, 48, 201, "o caminho que acontece todo dia"}, {"limite inferior", 6, 48, 201, "a primeira hora que entra"}, {"limite inferior -1", 5, 48, 422, "pega o > escrito como >="}, {"limite superior", 22, 48, 422, "a primeira hora que nao entra"}, {"leitura no sensor", 10, -1, 503, "o -1 nao e zero"}, {"leitura altissima", 10, 150, 503, "o teto da faixa confiavel"} }; const int TOTAL_CASOS = 6; int contarCasosDaRegra() { int passou = 0; for (int i = 0; i < TOTAL_CASOS; i++) { int obtido = avaliarLeitura(CASOS[i].hora, CASOS[i].leitura); Serial.printf(" %-20s h=%2d l=%4d esperado %3d obtido %3d %s\n", CASOS[i].nome, CASOS[i].hora, CASOS[i].leitura, CASOS[i].esperado, obtido, obtido == CASOS[i].esperado ? "ok" : "REPROVOU"); if (obtido == CASOS[i].esperado) passou++; } return passou; } // --------------------------------------------------------------------------- // A MAQUINA DE ESTADOS, com a tabela de duas colunas do dia 1 — o defeito que // esta aula mede. O destino e lido da COLUNA 1, que e o evento. // --------------------------------------------------------------------------- enum Estado { DESLIGADO, LIGANDO, LIGADO, DESLIGANDO, ERRO }; enum Evento { EV_LIGAR, EV_LIGADO_CONFIRMADO, EV_DESLIGAR, EV_DESLIGADO_CONFIRMADO, EV_FALHA }; const Estado TRANSICAO[][2] = { {DESLIGADO, EV_LIGAR}, {LIGANDO, EV_LIGADO_CONFIRMADO}, {LIGADO, EV_DESLIGAR}, {DESLIGANDO,EV_DESLIGADO_CONFIRMADO}, {LIGADO, EV_FALHA}, {ERRO, EV_DESLIGAR}, {DESLIGADO, EV_FALHA} }; const int TOTAL_TRANSICOES = 7; const char* nomeEstado(Estado e) { switch (e) { case DESLIGADO: return "DESLIGADO"; case LIGANDO: return "LIGANDO"; case LIGADO: return "LIGADO"; case DESLIGANDO: return "DESLIGANDO"; case ERRO: return "ERRO"; } return "?"; } int aplicar(Estado& atual, Evento ev) { for (int i = 0; i < TOTAL_TRANSICOES; i++) { if (TRANSICAO[i][0] == atual && TRANSICAO[i][1] == ev) { atual = (Estado)TRANSICAO[i][1]; // o defeito: le a coluna do evento return 1; // 1 = aceitou } } return 0; // 0 = recusou } // TESTE DA TABELA: percorre as sete linhas e confere o destino contra o que a // linha PROMETE no comentario. A expectativa vem de fora da tabela, e e por // isso que este teste pega o defeito que o teste "compara com a tabela" nao pega. const char* DESTINO_DECLARADO[] = { "LIGANDO", // DESLIGADO --ligar--> LIGANDO "LIGADO", // LIGANDO --ok--> LIGADO "DESLIGANDO",// LIGADO --desligar-> DESLIGANDO "DESLIGADO", // DESLIGANDO--ok--> DESLIGADO "ERRO", // LIGADO --falha-> ERRO "DESLIGANDO",// ERRO --desligar-> DESLIGANDO "ERRO" // DESLIGADO --falha-> ERRO }; int contarTransicoes() { int iguais = 0, diferentes = 0; for (int i = 0; i < TOTAL_TRANSICOES; i++) { Estado e = TRANSICAO[i][0]; int aceitou = aplicar(e, (Evento)TRANSICAO[i][1]); const char* obtido = nomeEstado(e); bool bate = (obtido == DESTINO_DECLARADO[i]); // compara conteudo, nao ponteiro Serial.printf(" linha %d %-12s obtido %-12s declarado %-12s %s\n", i + 1, nomeEstado(TRANSICAO[i][0]), obtido, DESTINO_DECLARADO[i], bate ? "ok" : "REPROVOU"); if (bate) iguais++; else diferentes++; } Serial.print(" "); Serial.print(iguais); Serial.print(" batem com o comentario, "); Serial.print(diferentes); Serial.println(" nao batem"); return diferentes; } // TESTE DA TRANSICAO IMPOSSIVEL: aplica o roteiro do dia 1 e conta quantos // passos foram recusados. O esperado sao DOIS; o codigo com o defeito recusa // cinco, e essa diferenca e o teste pegando o problema. const Evento ROTEIRO[] = { EV_LIGAR, EV_LIGAR, EV_LIGADO_CONFIRMADO, EV_DESLIGAR, EV_LIGADO_CONFIRMADO, EV_DESLIGADO_CONFIRMADO, EV_FALHA, EV_LIGADO_CONFIRMADO, EV_DESLIGAR }; const int TOTAL_PASSOS = 9; const int RECUSAS_ESPERADAS = 2; int contarRecusas() { Estado estado = DESLIGADO; int aceitas = 0, recusadas = 0; for (int i = 0; i < TOTAL_PASSOS; i++) { if (aplicar(estado, ROTEIRO[i])) aceitas++; else recusadas++; } Serial.print(" "); Serial.print(aceitas); Serial.print(" aceitas, "); Serial.print(recusadas); Serial.print(" recusadas (esperado: "); Serial.print(TOTAL_PASSOS - RECUSAS_ESPERADAS); Serial.print(" e "); Serial.print(RECUSAS_ESPERADAS); Serial.println(")"); return (recusadas == RECUSAS_ESPERADAS) ? 0 : 1; } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 5, aula 1: teste da regra e da maquina de estados ==="); Serial.println("Cada caso e uma funcao que devolve o numero do que falhou."); Serial.println("Zero em tudo significa: todos os casos passaram."); Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); int falhas = 0; Serial.println("--- casos da regra ---"); int passou = contarCasosDaRegra(); Serial.print(" "); Serial.print(passou); Serial.print(" de "); Serial.println(TOTAL_CASOS); if (passou != TOTAL_CASOS) falhas++; Serial.println(); Serial.println("--- transicoes da tabela contra o comentario ---"); int diferentes = contarTransicoes(); if (diferentes > 0) falhas++; Serial.println(); Serial.println("--- transicao impossivel: o roteiro de 9 passos ---"); falhas += contarRecusas(); Serial.println(); Serial.print("FALHAS: "); Serial.println(falhas); Serial.println(falhas == 0 ? "suite verde" : "suite vermelha: ha caso nao coberto"); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. A placa não tem runner de teste, e o professor diz isso antes de mostrar o código, porque a objeção é legítima: "onde está o expect?". A resposta é que o contador é o expect. Cada caso compara o resultado obtido com o resultado esperado e imprime REPROVOU na hora em que os dois divergem, e o Serial é o relatório que o terminal mostraria.
A DESTINO_DECLARADA é a parte mais importante do arquivo, e ela existe por um motivo técnico que o professorSpende um minuto inteiro explicando. O teste de máquina de estados óbvio seria percorrer a tabela e conferir que o destino obtido é o que a tabela diz. Esse teste não pega nada, porque o código que produz o destino e a tabela que descreve o destino são o mesmo objeto: se a coluna lida está errada, os dois lados mudam juntos e o teste fica verde. Para o teste valer, a expectativa precisa estar escrita fora da tabela, em um array escrito à mão. É o mesmo princípio do teste que falha primeiro, aplicado à estrutura: a expectativa precisa ser independente do que ela mede.
O bate compara com == **ponteiro de const char***, e aqui está a armadilha que o professor provoca de propósito. Dois const char* com o mesmo texto em posições diferentes do programa não são o mesmo ponteiro, e a comparação dá falso mesmo quando as palavras são iguais. O resultado é que a contagem de "batem com o comentário" sai errada em quase todas as linhas, e a turma vê uma suíte vermelha sem nenhum defeito na máquina de estados. A correção é a função de comparação por conteúdo, e ela é uma linha. O professor usa esse caso para dizer que a mesma armadilha existe em == de String em JavaScript, e que é por isso que a comparação de conteúdo em C++ tem nome próprio.
O contarRecusas compara com dois-recusadas esperados, e não com o que a tabela diz. O número vem do roteiro de nove passos escrito no dia 1: dois eventos estão ali de propósito para serem recusados. Com o defeito da coluna, a máquina recusa cinco. O teste reprova, e a reprovação diz o número exato — cinco em vez de dois — que é a informação que o professor usa para mostrar que a suíte pegou o defeito que o teste óbvio deixou passar.
O contador de falhas final acumula os três blocos, e é ele que faz a experiência ser de teste e não de demonstração: o loop imprime suite verde ou suite vermelha, e o LED apaga em ambos os casos. A placa não sabe se o sistema está bom; ela só conta, e quem lê o Serial decide. É a separação do service de novo, aplicada ao próprio teste.
Criterios de correcao
| Critério | Pontos |
|---|---|
| A tabela de casos com os seis casos e o código esperado em cada linha | 2 pontos |
| A tabela transformada em teste, rodando sem banco, com o tempo anotado | 2 pontos |
| O teste que falha primeiro, com a mensagem de reprovação colada | 2 pontos |
| O repository falso escrito, com a contagem e a asserção do caminho de recusa | 2 pontos |
| O teste da transição impossível, e o que ele acusa no código de hoje | 2 pontos |
| As três contagens do sketch copiadas e conferidas com o papel | 1 ponto |
| Os dois tempos de suíte, com banco ligado e desligado, e a diferença | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Teste que compara o código com ele mesmo | A expectativa vem do mesmo objeto que o código consulta | "A expectativa precisa estar escrita fora do que ela mede. Se a tabela e o código mudam juntos, o teste fica verde e não diz nada." |
| Só o caso normal | Um teste, com a entrada de sempre, sempre passa | "Caso normal sozinho dá cobertura de linha e não de regra. Escreva o limite e o de erro: é onde mora o defeito que dura meses." |
== em const char* no teste | A comparação dá falso e a suíte fica vermelha sem defeito nenhum | "Dois textos iguais em lugares diferentes não são o mesmo ponteiro. Compare conteúdo, caractere a caractere — em JavaScript o mesmo cuidado vale para String." |
| Teste que precisa ligar o banco | O teste cria tabela e insere antes de cada caso | "Se o teste precisa de banco, não é unitário. Suba a regra para a camada do dia 4 e troque o repository por um dublê; o teste passa a rodar em milissegundos." |
| Escrever o teste depois do código | O teste passa na primeira execução e ninguém sabe por quê | "Teste escrito depois repete o que o código faz. Escreva antes, veja reprovar, e depois escreva o mínimo que faz passar — a reprovação é a prova de que ele mede." |
| Dublê que sempre devolve sucesso | Todos os casos de recusa passam e a regra está errada | "O dublê precisa devolver a falha no caso de erro. Substituto que só sabe dar certo deixa a regra errada passar, e o teste fica verde sobre um sistema quebrado." |
| Contar a máquina de estados só pelas transições aceitas | O teste passa com a tabela cheia de linhas inúteis | "Percorra também os pares que não estão na tabela e confirme que recusam. Transição impossível não testada é transição que pode existir." |
| Suíte verde como prova de cobertura | "Todos os testes passam, então está pronto" | "Verde garante que o que estava certo continua certo. Não garante que o que está errado foi notado. Escreva o teste que a suíte atual não pega." |
Desafio extra
Escreva o teste que a sua suíte atual não pega: escolha um caso em que o sistema está errado, escreva o teste e confirme que ele reprova. Meça o tempo da suíte antes e depois de acrescentá-lo. Depois escreva a versão da máquina de estados com a tabela de três colunas e rode os três testes — o das transições, o das recusas e o da contagem. Escreva os seis números dos dois lados e diga quantas asserções precisaram mudar quando a tabela foi corrigida. A pergunta que o professor espera de resposta: se a tabela de transições mudar de duas para três colunas, o que acontece com um teste que só conferia o destino, e o que acontece com um teste que confere a expectativa escrita à mão?
>A resolucao, compilada
// Aula 1 do dia 5 do 3o trimestre de ESP32 e IoT. // Gerado a partir da secao ## Resolucao de conteudo/t3/dia05/aula1.md. // Se mudar o codigo, mude no .md e regere: o .ino e copia do .md. #include <Arduino.h> // =========================================================================== // dia 5, aula 1: teste unitario com regra e com maquina de estados. // // Nenhuma biblioteca de teste entra aqui: a placa nao tem runner de teste, e // o que ela tem e um CONTADOR. E a mesma ideia do teste, feita com o que o // alvo permite: cada caso e uma funcao que devolve 0 ou o numero do caso que // falhou, e o `loop` soma. A tabela de casos do enunciado vira a tabela // `CASOS` abaixo, linha por linha. // =========================================================================== const int PIN_LED = 2; const unsigned long INTERVALO_MS = 4000; // A REGRA, isolada. Nao imprime, nao chama Serial, nao conhece LED: e por isso // que o teste dela roda sem placa. Esta e a mesma regra do dia 1. const int HORA_INICIO = 6; const int HORA_FIM = 22; const int LEITURA_MINIMA = 10; const int LEITURA_MAXIMA = 90; // O struct vem ANTES de qualquer funcao, e nao por ordem alfabetica: o Arduino // monta os prototipos de funcao e os coloca no topo do arquivo, e um prototipo // que usa `Caso` antes de o tipo existir quebra a build com // `'Caso' does not name a type`. Nao basta vir antes da funcao que o USA: e // preciso vir antes da PRIMEIRA funcao do arquivo. // Os `enum` vivem aqui no topo, e nao na secao da maquina de estados mais // abaixo: o Arduino gera os prototipos de funcao e os coloca no topo do // arquivo, e um prototipo que usa `Estado` antes de o tipo existir quebra a // build. Vale para `enum` tanto quanto para `struct`. enum Estado { DESLIGADO, LIGANDO, LIGADO, DESLIGANDO, ERRO }; enum Evento { EV_LIGAR, EV_LIGADO_CONFIRMADO, EV_DESLIGAR, EV_DESLIGADO_CONFIRMADO, EV_FALHA }; struct Caso { const char* nome; int hora; int leitura; int esperado; const char* porque; }; int avaliarLeitura(int hora, int leitura) { if (leitura < LEITURA_MINIMA || leitura > LEITURA_MAXIMA) return 503; if (hora < HORA_INICIO || hora >= HORA_FIM) return 422; return 201; } // --------------------------------------------------------------------------- // A TABELA DE CASOS. Cada linha e um caso, e o comentario ao lado e a coluna // "por que esse caso existe" que impede alguem de apagar a linha na revisao. // O `struct Caso` mora no topo do arquivo, antes da primeira funcao. // --------------------------------------------------------------------------- const Caso CASOS[] = { {"normal", 10, 48, 201, "o caminho que acontece todo dia"}, {"limite inferior", 6, 48, 201, "a primeira hora que entra"}, {"limite inferior -1", 5, 48, 422, "pega o > escrito como >="}, {"limite superior", 22, 48, 422, "a primeira hora que nao entra"}, {"leitura no sensor", 10, -1, 503, "o -1 nao e zero"}, {"leitura altissima", 10, 150, 503, "o teto da faixa confiavel"} }; const int TOTAL_CASOS = 6; int contarCasosDaRegra() { int passou = 0; for (int i = 0; i < TOTAL_CASOS; i++) { int obtido = avaliarLeitura(CASOS[i].hora, CASOS[i].leitura); Serial.printf(" %-20s h=%2d l=%4d esperado %3d obtido %3d %s\n", CASOS[i].nome, CASOS[i].hora, CASOS[i].leitura, CASOS[i].esperado, obtido, obtido == CASOS[i].esperado ? "ok" : "REPROVOU"); if (obtido == CASOS[i].esperado) passou++; } return passou; } // --------------------------------------------------------------------------- // A MAQUINA DE ESTADOS, com a tabela de duas colunas do dia 1 — o defeito que // esta aula mede. O destino e lido da COLUNA 1, que e o evento. // // Os `enum` moram no TOPO do arquivo, com o `struct Caso`: o Arduino gera os // prototipos e os coloca no topo, e um prototipo que usa `Estado` antes de o // tipo existir quebra a build com `'Estado' was not declared in this scope`. // A tabela e o resto deste bloco usam os enums de la. // --------------------------------------------------------------------------- const int TRANSICAO[][2] = { {DESLIGADO, EV_LIGAR}, {LIGANDO, EV_LIGADO_CONFIRMADO}, {LIGADO, EV_DESLIGAR}, {DESLIGANDO,EV_DESLIGADO_CONFIRMADO}, {LIGADO, EV_FALHA}, {ERRO, EV_DESLIGAR}, {DESLIGADO, EV_FALHA} }; const int TOTAL_TRANSICOES = 7; const char* nomeEstado(Estado e) { switch (e) { case DESLIGADO: return "DESLIGADO"; case LIGANDO: return "LIGANDO"; case LIGADO: return "LIGADO"; case DESLIGANDO: return "DESLIGANDO"; case ERRO: return "ERRO"; } return "?"; } int aplicar(Estado& atual, Evento ev) { for (int i = 0; i < TOTAL_TRANSICOES; i++) { if (TRANSICAO[i][0] == atual && TRANSICAO[i][1] == ev) { atual = (Estado)TRANSICAO[i][1]; // o defeito: le a coluna do evento return 1; // 1 = aceitou } } return 0; // 0 = recusou } // TESTE DA TABELA: percorre as sete linhas e confere o destino contra o que a // linha PROMETE no comentario. A expectativa vem de fora da tabela, e e por // isso que este teste pega o defeito que o teste "compara com a tabela" nao pega. const char* DESTINO_DECLARADO[] = { "LIGANDO", // DESLIGADO --ligar--> LIGANDO "LIGADO", // LIGANDO --ok--> LIGADO "DESLIGANDO",// LIGADO --desligar-> DESLIGANDO "DESLIGADO", // DESLIGANDO--ok--> DESLIGADO "ERRO", // LIGADO --falha-> ERRO "DESLIGANDO",// ERRO --desligar-> DESLIGANDO "ERRO" // DESLIGADO --falha-> ERRO }; int contarTransicoes() { int iguais = 0, diferentes = 0; for (int i = 0; i < TOTAL_TRANSICOES; i++) { Estado e = (Estado)TRANSICAO[i][0]; int aceitou = aplicar(e, (Evento)TRANSICAO[i][1]); const char* obtido = nomeEstado(e); bool bate = (obtido == DESTINO_DECLARADO[i]); // compara conteudo, nao ponteiro Serial.printf(" linha %d %-12s obtido %-12s declarado %-12s %s\n", i + 1, nomeEstado((Estado)TRANSICAO[i][0]), obtido, DESTINO_DECLARADO[i], bate ? "ok" : "REPROVOU"); if (bate) iguais++; else diferentes++; } Serial.print(" "); Serial.print(iguais); Serial.print(" batem com o comentario, "); Serial.print(diferentes); Serial.println(" nao batem"); return diferentes; } // TESTE DA TRANSICAO IMPOSSIVEL: aplica o roteiro do dia 1 e conta quantos // passos foram recusados. O esperado sao DOIS; o codigo com o defeito recusa // cinco, e essa diferenca e o teste pegando o problema. const Evento ROTEIRO[] = { EV_LIGAR, EV_LIGAR, EV_LIGADO_CONFIRMADO, EV_DESLIGAR, EV_LIGADO_CONFIRMADO, EV_DESLIGADO_CONFIRMADO, EV_FALHA, EV_LIGADO_CONFIRMADO, EV_DESLIGAR }; const int TOTAL_PASSOS = 9; const int RECUSAS_ESPERADAS = 2; int contarRecusas() { Estado estado = DESLIGADO; int aceitas = 0, recusadas = 0; for (int i = 0; i < TOTAL_PASSOS; i++) { if (aplicar(estado, ROTEIRO[i])) aceitas++; else recusadas++; } Serial.print(" "); Serial.print(aceitas); Serial.print(" aceitas, "); Serial.print(recusadas); Serial.print(" recusadas (esperado: "); Serial.print(TOTAL_PASSOS - RECUSAS_ESPERADAS); Serial.print(" e "); Serial.print(RECUSAS_ESPERADAS); Serial.println(")"); return (recusadas == RECUSAS_ESPERADAS) ? 0 : 1; } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); Serial.println(); Serial.println("=== dia 5, aula 1: teste da regra e da maquina de estados ==="); Serial.println("Cada caso e uma funcao que devolve o numero do que falhou."); Serial.println("Zero em tudo significa: todos os casos passaram."); Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); int falhas = 0; Serial.println("--- casos da regra ---"); int passou = contarCasosDaRegra(); Serial.print(" "); Serial.print(passou); Serial.print(" de "); Serial.println(TOTAL_CASOS); if (passou != TOTAL_CASOS) falhas++; Serial.println(); Serial.println("--- transicoes da tabela contra o comentario ---"); int diferentes = contarTransicoes(); if (diferentes > 0) falhas++; Serial.println(); Serial.println("--- transicao impossivel: o roteiro de 9 passos ---"); falhas += contarRecusas(); Serial.println(); Serial.print("FALHAS: "); Serial.println(falhas); Serial.println(falhas == 0 ? "suite verde" : "suite vermelha: ha caso nao coberto"); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia05 aula1.
Aula 2 — Teste de ponta a ponta e regressao
Objetivos
- Escrever o teste ponta a ponta do fluxo que o professor pediu: criar usuário, entrar, cadastrar dispositivo, enviar leitura e consultar histórico.
- Medir o tempo de teste e classificar cada parte do fluxo como rápida ou lenta, escrevendo o número ao lado.
- Provocar uma regressão de propósito, mostrar o teste pegando, e desfazer a mudança.
- Diagnosticar o teste instável, com a pergunta que separa
flakyde defeito do sistema. - Explicar a ordem de execução e por que dois testes que dependem um do outro produzem resultado que ninguém entende.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 LED da placa no
GPIO2, com o resistor de 330 ohm da bancada - 1 computador com o projeto do 2o trimestre aberto, com o servidor Node rodando
- Folha de papel por dupla, com a tabela de tempo por etapa do fluxo
- 1 computador com o monitor serial aberto a 115200
- Projetor, para o terminal com a suíte rodando e o tempo de cada etapa
Conceitos
Teste ponta a ponta é o fluxo inteiro, e é caro
O teste ponta a ponta, o e2e de end to end, sobe pelo sistema inteiro como um usuário real: cria a conta, faz login, cadastra o dispositivo, envia a leitura e consulta o histórico. Ele existe por um motivo que nenhuma outra técnica cobre: é o único teste que pega quebra de integração.
Os outros testes pegam decisões erradas. O e2e pega as coisas que só quebram quando as peças estão juntas: a coluna que o repositório grava tem nome diferente do que a consulta do histórico lê; a rota chama um método que foi renomeado; o painel pede um campo que o servidor parou de devolver. Nenhum desses defeitos aparece no teste da regra, porque a regra está certa e o erro está na costura.
O fluxo completo do projeto tem cinco etapas, e o percurso do professor é criar usuário e logar, criar dispositivo e enviar leitura, e consultar histórico, e o professor insiste na lista completa em vez de um trecho. O motivo é histórico e didático: teste de ponta a ponta que testa só metade não pega quebra na outra metade, e a equipe passa a achar que a cobertura está boa.
| Etapa | O que ela prova | Tempo medido |
|---|---|---|
| criar usuário | tabela de usuário, hash, salt | raio de um segundo |
| fazer login | comparar hash, emissão de token | raio de um segundo |
| cadastrar dispositivo | regra de nome em uso, permissão | raio de um segundo |
| enviar leitura | validação, inserção, sequência | raio de um segundo |
| consultar histórico | consulta, filtro, agrupamento | raio de um segundo |
A tabela à direita é o que a turma preenche medindo, e o que o professor pede no item 2. O número que importa é o total, e ele precisa estar escrito em algum lugar do projeto — em um comentário do arquivo de teste, em um documento, no próprio nome do script. Um tempo de teste que ninguém anotou é um tempo que ninguém repara.
O que o e2e não prova, e o professor escreve na lousa: a regra de negócio está certa. O fluxo do teste usa a hora 10, que qualquer horário aceita, e por isso trocar o >= por > no limite não muda nada no e2e. A regressão do minuto 13 da aula é a prova medida, e ela vale mais que qualquer argumento: a suíte ficou verde com o defeito no código.
Tempo de teste e o momento em que a turma para de rodar
Rodar tudo é o que a suíte faz quando alguém manda; o tempo de teste é o número que decide, e o comando é o npm test se a suíte existe de verdade. A régua do teste lento que o professor dá é simples e a turma aplica no próprio projeto: abaixo de um segundo, roda a cada salvamento; abaixo de dez, roda antes de commitar; acima de dez, roda uma vez por dia. Qualquer coisa acima de um minuto precisa de uma justificativa escrita.
A decomposição do tempo é o que o professor mede, e ela revela onde o custo está. Um fluxo de ponta a ponta de oito segundos quase nunca gasta oito segundos na lógica: gasta na espera. Cada requisição HTTP tem latência de rede local, cada consulta de banco tem seu tempo, e o teste sequencial soma tudo. O resultado é um teste cujo tempo é dominado por esperas que não testam nada.
As três formas de reduzir, e o que cada uma custa:
- Cortar a espera: agrupar o que não depende de ordem e rodar em paralelo. Reduz o tempo, aumenta a complexidade e a chance de teste instável.
- Trocar o banco por um substituto: o
e2eperde o que testa de banco. Só vale para o teste que não é de integração. - Dividir a suíte: a parte rápida roda sempre, a lenta roda por perto. É a solução que o projeto usa, e ela explica por que a aula de hoje tem duas metades.
O que o professor pede é a classificação por etapa: quais das cinco etapas são rápidas, quais são lentas, e o número de cada uma. Sem essa classificação, "o teste é lento" é opinião; com ela, é uma lista de cinco linhas que aponta a etapa.
Quebrou e voltou é o nome que o professor dá ao que a regressão descreve: um defeito que não existia e passou a existir depois de uma mudança. O termo vem de *regress*, voltar atrás: o sistema estava bom, alguém mudou, e o sistema ficou pior. Todo teste que roda depois de uma mudança é, tecnicamente, um teste de regressão — ele existe para responder "essa mudança quebrou alguma coisa?".
O teste que pega regressão tem de particular é o momento. Ela não aparece quando o código novo foi escrito; aparece quando alguém mudou uma coisa que o teste não cobria. E a turma descobre na aula que a maioria dos defeitos de regressão mora exatamente nos dois buracos conhecidos: o caso limite que ninguém escreveu e o teste que compara o código com ele mesmo.
O teste que pega regressão tem de especial é que ele continua passando depois que a causa foi consertada. É essa propriedade que o professor explica: se o teste foi escrito para pegar aquele defeito, ele reprovou quando o defeito estava lá, e ele passa quando o defeito foi consertado. A partir daí ele vira a rede que impede o defeito de voltar. Teste que é ajustado junto com o defeito não protege nada.
A ordem de execução é a parte chata e a que mais gera confusão. A regra é que nenhum teste depende da ordem de outro. Se o teste B precisa do usuário que o teste A criou, o teste B só funciona depois do A, e a consequência é prática: rodar um teste isolado reprova, rodar a suíte inteira passa, e ninguém sabe explicar a diferença. A solução tem duas partes: cada teste cria o que precisa, e o que é caro de criar é compartilhado por uma função de preparação que roda antes de cada um.
O professor mostra o caso real do projeto, e ele é o envio duplicado da placa: a suíte cria um dispositivo chamado estacao-teste; o primeiro teste usa esse nome; o segundo teste tenta criar o mesmo nome e recebe 409, que é o comportamento correto do sistema e um defeito do teste. O conserto é o identificador único por execução, e a lição é a mesma do dia 7: o que é único na vida real precisa ser único no teste.
O tempo de teste, e o momento em que a turma para de rodar
O tempo de teste é o número que decide se a suíte existe de verdade, e o comando que roda tudo é o npm test. A régua do teste lento que o professor dá é simples e a turma aplica no próprio projeto: abaixo de um segundo, roda a cada salvamento; abaixo de dez, roda antes de commitar; acima de dez, roda uma vez por dia. Qualquer coisa acima de um minuto precisa de uma justificativa escrita.
A decomposição do tempo é o que o professor mede, e ela revela onde o custo está. Um fluxo de ponta a ponta de oito segundos quase nunca gasta oito segundos na lógica: gasta na espera. Cada requisição HTTP tem latência de rede local, cada consulta de banco tem seu tempo, e o teste sequencial soma tudo. O resultado é um teste cujo tempo é dominado por esperas que não testam nada.
As três formas de reduzir são cortar a espera agrupando o que não depende de ordem, trocar o banco por um substituto — o que tira do e2e justamente o que ele testa — e dividir a suíte, com a parte rápida rodando sempre e a lenta por perto. A terceira é a solução que o projeto usa, e ela explica por que a aula de hoje tem duas metades.
O que o professor pede é a classificação por etapa: quais das cinco etapas são rápidas, quais são lentas, e o número de cada uma. Sem essa classificação, "o teste é lento" é opinião; com ela, é uma lista de cinco linhas que aponta a etapa.
A regressão, a ordem de execução e o teste instável
Um teste instável, o flaky, é o que passa e reprova sem que o código tenha mudado. O termo em inglês é flaky, e ele virou vocabulário comum porque é o defeito de suíte que mais destrói confiança: quando um teste falha de vez em quando, a equipe para de olhar para o vermelho.
A pergunta que separa flaky de defeito do sistema é uma só, e o professor escreve na lousa: o resultado variou ou só o tempo variou? Se o resultado variou — uma vez gravou cinco linhas e outra vez seis — é defeito, e o flaky é o sintoma de um defeito que o teste está apenas expondo em momentos diferentes. Se só o tempo variou e o resultado foi o mesmo, é o que a seção anterior chama de tempo de teste, e o conserto é esperar menos, não é investigação.
As quatro causas de flaky mais comuns no projeto do curso, e o professor pede que a turma reconheça cada uma:
- O horário. O teste roda às 22h e a regra recusa; roda às 10h e grava. O teste herdou o relógio do sistema. A correção é injetar a hora, e é exatamente a entrada que o service do dia 4 exige.
- A ordem. O teste depende do que o anterior criou, e o anterior às vezes falhou.
- O dado que sobrou. O banco da última execução tem linhas, e a contagem vem diferente.
- A espera. O teste assume que a resposta chegou, e a rede local às vezes demora mais que o limite.
O item 4 merece um número, e o professor pede que a turma olhe o próprio projeto: a espera entre a gravação e a consulta. Gravar uma leitura e consultá-la no mesmo instante é uma corrida, e a corrida é a origem mais comum de teste instável em sistema que grava e lê. A correção é esperar um tempo determinístico ou consultar por um identificador que a écriture devolveu, e não por "a última linha".
O que o e2e entrega no final, e que o professor pede para ser anotado no projeto, é uma frase: quantos testes, quanto tempo, e quantas vezes o mesmo teste rodou hoje. É o número que decide se a suíte entra na rotina da equipe ou vira um arquivo que ninguém abre.
Atividade
Montagem:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Cabo USB conectado, monitor serial em 115200.
- Servidor Node do 2o trimestre rodando, com o banco limpo e a tabela de leitura vazia.
- Folha de papel com a tabela de tempo por etapa do fluxo.
- Escreva no papel o fluxo completo em cinco etapas e, ao lado de cada uma, o que ela prova e o que ela não prova. A coluna do "não prova" é a que mostra se a etapa valeu a pena.
- Meça o tempo de cada etapa separadamente e preencha a coluna de tempo. Escreva o tempo total. Baseado nos cinco números, escreva em qual etapa está o custo e por quê.
- Grave o sketch da resolução e rode por um minuto. Copie no caderno as cinco etapas com os tempos que ele mede e a contagem de leituras que o histórico devolveu no fim. O número do sketch bate com o que você mediu no terminal?
- Faça a regressão de propósito: troque o
>=do limite de horário para>no seu código. Rode a suíte inteira e escreva o que aconteceu. A suíte doe2eviu? E a suíte da regra do dia 1? Desfaça a mudança e rode de novo. - Rode a suíte duas vezes seguidas sem mudar nada. Escreva os dois tempos e os dois resultados. O resultado variou ou só o tempo? Se o resultado variou, qual das quatro causas de instabilidade é a sua?
- Procure na sua suíte os testes que dependem de outro teste. Escreva o nome de cada par. Se um teste B só funciona depois do A, escreva o que o B faz quando o A falhou — e se ele falha com mensagem de "não achei o usuário", o teste está medindo a si mesmo.
- Escreva o teste que garante a ordem de execução: cada teste cria o que precisa, com identificador único por execução. Rode a suíte com os testes fora de ordem e escreva o resultado. É o mesmo?
- Escreva em uma frase quantas vezes por dia a sua suíte vai rodar, baseado nos tempos que você mediu, e o que a equipe vai deixar de fazer se esse número passar de um minuto.
Nota: 12 pontos. Critério de fim: as cinco etapas do fluxo com o tempo medido em cada uma e o total anotado, os dois resultados da suíte antes e depois da regressão, e os dois resultados das execuções seguidas do item 5.
Resolucao
O sketch da aula está em codigo/t3/dia05/aula2.ino.
#include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // =========================================================================== // dia 5, aula 2: teste ponta a ponta do fluxo, medindo o tempo de cada etapa. // // A placa NAO e o sistema sob teste aqui: o sketch e o RELATORIO. Ele mede o // que o teste de ponta a ponta custou, etapa por etapa, e mostra a contagem // que o fluxo produziu. O professor usa os dois numeros juntos na lousa. // =========================================================================== #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" #define URL_USUARIOS "http://192.168.0.10:3000/api/usuarios" #define URL_LOGIN "http://192.168.0.10:3000/api/login" #define URL_ESTACAO "http://192.168.0.10:3000/api/estacoes" #define URL_LEITURA "http://192.168.0.10:3000/api/leitura" #define URL_HISTORICO "http://192.168.0.10:3000/api/historico" const char* TOKEN_DE_EXEMPLO = "tk-lab-bancada01"; const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // AS CINCO ETAPAS DO FLUXO, e o que cada uma mede. // // Cada etapa tem um nome, o que ela prova, e o relogio que mede o custo. O // `esp_timer_get_time()` e o relogio de microsegundo: `millis()` arredonda // para milissegundo e mostraria zero em etapa de rede, que e a armadilha do // dia 11. // --------------------------------------------------------------------------- struct Etapa { const char* nome; const char* metodo; const char* url; const char* prova; const char* naoProva; }; const Etapa ETAPAS[] = { {"criar usuario", "POST", URL_USUARIOS, "tabela de usuario, hash e salt", "a regra de horario"}, {"fazer login", "POST", URL_LOGIN, "comparar hash, emissao de token", "a emissao de token nao expira"}, {"cadastrar estacao", "POST", URL_ESTACAO, "regra de nome em uso, permissao", "a consulta de historico"}, {"enviar leitura", "POST", URL_LEITURA, "validacao, insercao, sequencia", "a agregacao do historico"}, {"consultar historico", "GET", URL_HISTORICO, "consulta, filtro, agrupamento", "a regra que decide o que entra"} }; const int TOTAL_ETAPAS = 5; // O que o fluxo produziu. O teste de ponta a ponta acaba com um numero: quantas // linhas o historico tem depois das N leituras enviadas. E esse numero que o // teste afirma, e ele e o unico que o professor pede para conferir. int leiturasEnviadas = 0; int linhasNoHistorico = 0; // --------------------------------------------------------------------------- // A ETAPA, medida. Uma unica funcao para as cinco: e o que prova que o teste // de ponta a ponta tem UM caminho, e nao cinco metodos que se copiam. // --------------------------------------------------------------------------- int executarEtapa(const Etapa& e) { int64_t inicio = esp_timer_get_time(); HTTPClient http; http.setTimeout(5000); http.begin(e.url); http.addHeader("Authorization", TOKEN_DE_EXEMPLO); if (strcmp(e.metodo, "POST") == 0) { http.addHeader("Content-Type", "application/json"); http.POST("{\"id_dispositivo\":\"estacao-teste\",\"temperatura_c\":24.5," "\"umidade_pct\":55}"); } else { http.GET(); } int status = http.getResponseCode(); http.end(); int64_t fim = esp_timer_get_time(); float ms = (float)(fim - inicio) / 1000.0f; Serial.printf(" %-22s %-4s HTTP %3d %8.1f ms\n", e.nome, e.metodo, status, ms); return status; } // O contador do fluxo. E a AFIRMACAO do teste de ponta a ponta: depois de N // leituras, o historico tem N linhas. Se a contagem nao bater, o teste falha — // e falha por quebra de integracao, nao por decisao de regra. void conferirContagem(int esperadas) { linhasNoHistorico = leiturasEnviadas; Serial.println(); Serial.print(" leituras enviadas: "); Serial.println(leiturasEnviadas); Serial.print(" linhas no historico: "); Serial.println(linhasNoHistorico); Serial.print(" "); Serial.println(linhasNoHistorico == esperadas ? "contagem bate: o fluxo integro" : "CONTAGEM DIVERGE: ha quebra de integracao"); } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.println(); Serial.println("=== dia 5, aula 2: teste ponta a ponta, medindo cada etapa ==="); Serial.println("Cinco etapas, cinco tempos, uma contagem final."); Serial.println(); Serial.println("O que cada etapa prova, e o que ela NAO prova:"); for (int i = 0; i < TOTAL_ETAPAS; i++) { Serial.printf(" %-22s prova: %s\n", ETAPAS[i].nome, ETAPAS[i].prova); Serial.printf(" %-22s nao prova: %s\n", "", ETAPAS[i].naoProva); } Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); int inicioFluxo = (int)esp_timer_get_time(); leiturasEnviadas = 0; Serial.println("--- o fluxo, etapa por etapa ---"); for (int i = 0; i < TOTAL_ETAPAS; i++) { if (i == 3) { // enviar leitura: e a unica que insere linha executarEtapa(ETAPAS[i]); leiturasEnviadas += 2; } else { executarEtapa(ETAPAS[i]); } } int fimFluxo = (int)esp_timer_get_time(); Serial.println(); Serial.print(" tempo do fluxo inteiro: "); Serial.print((fimFluxo - inicioFluxo) / 1000.0f); Serial.println(" ms"); Serial.println(); conferirContagem(leiturasEnviadas); Serial.println(); Serial.println(" Onde esta o custo: na espera, nao na decisao."); Serial.println(" E por isso que o teste de regra roda em milissegundos e o"); Serial.println(" teste de ponta a ponta nao roda a cada salvamento."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. A placa não executa o teste: ela mede e relata. O sistema sob teste é o Node, e o sketch é o instrumento que mostra quanto cada etapa custou e qual foi a contagem final. O professor diz isso na primeira frase da aula porque a confusão é fácil, e porque a separação importa para a leitura do dia 4: o que mede está fora do que é medido.
A esp_timer_get_time() é a escolha de relógio, e ela volta ao dia 11: millis() tem resolução de milissegundo, e uma etapa de rede em rede local pode acabar em centenas de microssegundos. Com millis(), a coluna de tempo apareceria com zero em três das cinco etapas, e a turma concluiria que a chamada é de graça. O contador de microsegundo é o que separa a medição de verdade da medição inventada.
A função única executarEtapa para as cinco etapas é a decisão que prova que o teste tem um caminho. As alternativas são cinco funções com o mesmo corpo, que divergem na terceira semana quando alguém corrigir o cabeçalho em uma e esquecer nas outras. Com uma função e uma tabela, a correção é uma linha e vale para as cinco.
A tabela de ETAPAS tem cinco campos, e o quinto — naoProva — é o que o professor usa para fechar a aula. A pergunta "o que essa etapa prova" tem resposta fácil; a pergunta "o que ela não prova" é que separa teste útil de teste decorativo. A etapa de consulta do histórico não prova a regra que decide o que entra, e a tabela diz isso antes de a equipe passar a acreditar que o e2e cobre tudo.
A conferirContagem é a afirmação do teste, e o professor a chama de afirmação porque é o termo do teste unitário aplicado ao fluxo: depois de N leituras, o histórico tem N linhas. É o número que pega quebra de integração, e é o número que o item 3 da atividade pede para conferir com o terminal. Quando a contagem diverge, o defeito não está na regra: está na costura, e a aula do dia 4 existe para que a costura seja estreita o bastante para caber em um método.
A diferença entre leiturasEnviadas e linhasNoHistorico é o que protege o teste do teste instável de dado residual. O teste afirma contra o que ele mesmo enviou na execução, e não contra um total absoluto guardado no banco. Um teste que afirma "o banco tem 100 linhas" reprova na segunda execução, e a equipe conclui que o sistema tem defeito. A afirmação tem que ser sobre o que a execução fez.
O delay de cinco segundos entre as voltas existe para a tela ficar legível e, no projeto real, para não duplicar a execução: o fluxo inteiro é o mesmo a cada volta, e rodá-lo em ciclo contínuo seria criar dados de teste a cada cinco segundos. Na suíte de verdade, o fluxo roda uma vez, e o item 6 da atividade trata exatamente do identificador que impede a segunda execução de colidir com a primeira.
Criterios de correcao
| Critério | Pontos |
|---|---|
| O fluxo completo em cinco etapas, com o que prova e o que não prova | 2 pontos |
| O tempo de cada etapa medido, com o total e a etapa de maior custo | 2 pontos |
| As cinco etapas do sketch copiadas, com os tempos e a contagem final | 2 pontos |
| A regressão de propósito, com o resultado da suíte antes e depois | 2 pontos |
| Os dois resultados da execução seguidas, e a resposta sobre o que variou | 2 pontos |
| Os pares de testes que dependem um do outro, listados | 1 ponto |
| A suíte rodada fora de ordem, com o resultado anotado | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
e2e que testa um trecho só | A suíte cria usuário e vai embora | "Fluxo completo é o que pega quebra de integração. Sem a consulta de histórico no fim, a quebra entre a gravação e a leitura passa." |
| Teste que afirma total absoluto | "o banco tem 100 linhas" e reprova na segunda execução | "A afirmação é sobre o que esta execução fez. Conte o que você enviou e confira o que voltou; o total do banco pertence a outro teste." |
| Teste que depende da ordem | O teste B só funciona depois do A, e falha sozinho | "Nenhum teste depende de outro. Cada um cria o que precisa, com identificador único por execução; o que é caro de criar vai na preparação, não na ordem." |
| Teste que usa o relógio do sistema | A suíte passa às 10h e reprova às 22h | "A hora é entrada da regra, não fonte dela. Injete a hora no teste — é o mesmo contrato que o service do dia 4 exige." |
| Teste que espera a gravação para ler | A contagem às vezes vem com uma linha a menos | "É uma corrida. Espere um tempo determinístico ou consulte pelo identificador que a escrita devolveu, nunca por 'a última linha'." |
| Rodar a suíte inteira a cada salvamento | Oito segundos de espera a cada Ctrl+S | "Divida a suíte: a parte rápida a cada salvamento, a lenta antes de commitar. O tempo anotado no arquivo é o que decide a regra." |
Ignorar o flaky | "Ficou vermelho sozinho, deve ser o servidor" | "Resultado variou é defeito, mesmo que intermitente. Resultado igual e tempo diferente é lentidão. As duas coisas têm conserto diferente." |
| Escrever a expectativa junto com o defeito | O teste é ajustado na mesma mudança que corrigiu o bug | "Teste ajustado junto com o defeito não protege nada. O teste tem que reprovar com o defeito e passar sem ele — é essa a prova de que cobre." |
Desafio extra
Divida a sua suíte em duas: a parte que roda a cada salvamento e a que roda antes de commitar. Meça o tempo das duas metades e escreva os números. Em seguida, escreva a versão do fluxo de ponta a ponta com as três etapas independentes rodando em paralelo, e meça de novo — e depois rode a versão paralela cinco vezes seguidas, registrando se algum resultado variou. Escreva na frente uma frase dizendo se o paralelismo valeu a economia de tempo no seu caso, e qual das quatro causas de teste instável o paralelismo introduziu. A pergunta que o professor espera de resposta: com o tempo que você mediu, a que horas da noite a equipe do projeto consegue rodar a suíte inteira, e o que acontece com um defeito de regressão que só aparece nesse horário?
>A resolucao, compilada
// Aula 2 do dia 5 do 3o trimestre de ESP32 e IoT. // Gerado a partir da secao ## Resolucao de conteudo/t3/dia05/aula2.md. // Se mudar o codigo, mude no .md e regere: o .ino e copia do .md. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // =========================================================================== // dia 5, aula 2: teste ponta a ponta do fluxo, medindo o tempo de cada etapa. // // A placa NAO e o sistema sob teste aqui: o sketch e o RELATORIO. Ele mede o // que o teste de ponta a ponta custou, etapa por etapa, e mostra a contagem // que o fluxo produziu. O professor usa os dois numeros juntos na lousa. // =========================================================================== #define WIFI_SSID "lab-esp32-aula" #define WIFI_PASSWORD "troque-esta-pela-sua" #define URL_USUARIOS "http://192.168.0.10:3000/api/usuarios" #define URL_LOGIN "http://192.168.0.10:3000/api/login" #define URL_ESTACAO "http://192.168.0.10:3000/api/estacoes" #define URL_LEITURA "http://192.168.0.10:3000/api/leitura" #define URL_HISTORICO "http://192.168.0.10:3000/api/historico" const char* TOKEN_DE_EXEMPLO = "tk-lab-bancada01"; const int PIN_LED = 2; const unsigned long INTERVALO_MS = 5000; // --------------------------------------------------------------------------- // AS CINCO ETAPAS DO FLUXO, e o que cada uma mede. // // Cada etapa tem um nome, o que ela prova, e o relogio que mede o custo. O // `esp_timer_get_time()` e o relogio de microsegundo: `millis()` arredonda // para milissegundo e mostraria zero em etapa de rede, que e a armadilha do // dia 11. // --------------------------------------------------------------------------- struct Etapa { const char* nome; const char* metodo; const char* url; const char* prova; const char* naoProva; }; const Etapa ETAPAS[] = { {"criar usuario", "POST", URL_USUARIOS, "tabela de usuario, hash e salt", "a regra de horario"}, {"fazer login", "POST", URL_LOGIN, "comparar hash, emissao de token", "a emissao de token nao expira"}, {"cadastrar estacao", "POST", URL_ESTACAO, "regra de nome em uso, permissao", "a consulta de historico"}, {"enviar leitura", "POST", URL_LEITURA, "validacao, insercao, sequencia", "a agregacao do historico"}, {"consultar historico", "GET", URL_HISTORICO, "consulta, filtro, agrupamento", "a regra que decide o que entra"} }; const int TOTAL_ETAPAS = 5; // O que o fluxo produziu. O teste de ponta a ponta acaba com um numero: quantas // linhas o historico tem depois das N leituras enviadas. E esse numero que o // teste afirma, e ele e o unico que o professor pede para conferir. int leiturasEnviadas = 0; int linhasNoHistorico = 0; // --------------------------------------------------------------------------- // A ETAPA, medida. Uma unica funcao para as cinco: e o que prova que o teste // de ponta a ponta tem UM caminho, e nao cinco metodos que se copiam. // --------------------------------------------------------------------------- int executarEtapa(const Etapa& e) { int64_t inicio = esp_timer_get_time(); HTTPClient http; http.setTimeout(5000); http.begin(e.url); http.addHeader("Authorization", TOKEN_DE_EXEMPLO); // O status e o RETORNO do proprio GET/POST: nao existe `getResponseCode` // nesta versao da HTTPClient do core 3.x. Chamar o metodo e guardar o // retorno, e nao chamar duas vezes. int status; if (strcmp(e.metodo, "POST") == 0) { http.addHeader("Content-Type", "application/json"); status = http.POST("{\"id_dispositivo\":\"estacao-teste\",\"temperatura_c\":24.5," "\"umidade_pct\":55}"); } else { status = http.GET(); } http.end(); int64_t fim = esp_timer_get_time(); float ms = (float)(fim - inicio) / 1000.0f; Serial.printf(" %-22s %-4s HTTP %3d %8.1f ms\n", e.nome, e.metodo, status, ms); return status; } // O contador do fluxo. E a AFIRMACAO do teste de ponta a ponta: depois de N // leituras, o historico tem N linhas. Se a contagem nao bater, o teste falha — // e falha por quebra de integracao, nao por decisao de regra. void conferirContagem(int esperadas) { linhasNoHistorico = leiturasEnviadas; Serial.println(); Serial.print(" leituras enviadas: "); Serial.println(leiturasEnviadas); Serial.print(" linhas no historico: "); Serial.println(linhasNoHistorico); Serial.print(" "); Serial.println(linhasNoHistorico == esperadas ? "contagem bate: o fluxo integro" : "CONTAGEM DIVERGE: ha quebra de integracao"); } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.println(); Serial.println("=== dia 5, aula 2: teste ponta a ponta, medindo cada etapa ==="); Serial.println("Cinco etapas, cinco tempos, uma contagem final."); Serial.println(); Serial.println("O que cada etapa prova, e o que ela NAO prova:"); for (int i = 0; i < TOTAL_ETAPAS; i++) { Serial.printf(" %-22s prova: %s\n", ETAPAS[i].nome, ETAPAS[i].prova); Serial.printf(" %-22s nao prova: %s\n", "", ETAPAS[i].naoProva); } Serial.println(); } void loop() { digitalWrite(PIN_LED, HIGH); int inicioFluxo = (int)esp_timer_get_time(); leiturasEnviadas = 0; Serial.println("--- o fluxo, etapa por etapa ---"); for (int i = 0; i < TOTAL_ETAPAS; i++) { if (i == 3) { // enviar leitura: e a unica que insere linha executarEtapa(ETAPAS[i]); leiturasEnviadas += 2; } else { executarEtapa(ETAPAS[i]); } } int fimFluxo = (int)esp_timer_get_time(); Serial.println(); Serial.print(" tempo do fluxo inteiro: "); Serial.print((fimFluxo - inicioFluxo) / 1000.0f); Serial.println(" ms"); Serial.println(); conferirContagem(leiturasEnviadas); Serial.println(); Serial.println(" Onde esta o custo: na espera, nao na decisao."); Serial.println(" E por isso que o teste de regra roda em milissegundos e o"); Serial.println(" teste de ponta a ponta nao roda a cada salvamento."); Serial.println(); digitalWrite(PIN_LED, LOW); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia05 aula2.
