Teste a fundo — Arduino e IoT — semana 5 do 3o trimestre

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

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:

CasoEntradaResultado esperadoPor que esse caso existe
normalhora 10, leitura 48201, gravouo caminho que acontece todo dia
limite inferiorhora 6, leitura 48201, gravoua primeira hora que entra
limite inferior menos umhora 5, leitura 48422, recusadapega o > escrito como >=
limite superiorhora 22, leitura 48422, recusadaa primeira hora que não entra
faixa do sensorhora 10, leitura -1503, fonte falhouo -1 não é zero
dado fora de faixa altahora 10, leitura 150503, fonte falhouo 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:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • 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".
  1. 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.
  2. 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ê.
  3. 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?
  4. 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.
  5. 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 if ou a da tabela?
  6. 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.
  7. 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?
  8. 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érioPontos
A tabela de casos com os seis casos e o código esperado em cada linha2 pontos
A tabela transformada em teste, rodando sem banco, com o tempo anotado2 pontos
O teste que falha primeiro, com a mensagem de reprovação colada2 pontos
O repository falso escrito, com a contagem e a asserção do caminho de recusa2 pontos
O teste da transição impossível, e o que ele acusa no código de hoje2 pontos
As três contagens do sketch copiadas e conferidas com o papel1 ponto
Os dois tempos de suíte, com banco ligado e desligado, e a diferença1 ponto

Erros comuns

ErroComo apareceCorreção
Teste que compara o código com ele mesmoA 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 normalUm 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 testeA 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 bancoO 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ódigoO 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 sucessoTodos 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 aceitasO 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 flaky de 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.

EtapaO que ela provaTempo medido
criar usuáriotabela de usuário, hash, saltraio de um segundo
fazer logincomparar hash, emissão de tokenraio de um segundo
cadastrar dispositivoregra de nome em uso, permissãoraio de um segundo
enviar leituravalidação, inserção, sequênciaraio de um segundo
consultar históricoconsulta, filtro, agrupamentoraio 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 e2e perde 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:

  1. 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.
  2. A ordem. O teste depende do que o anterior criou, e o anterior às vezes falhou.
  3. O dado que sobrou. O banco da última execução tem linhas, e a contagem vem diferente.
  4. 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:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • 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.
  1. 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.
  2. 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ê.
  3. 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?
  4. 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 do e2e viu? E a suíte da regra do dia 1? Desfaça a mudança e rode de novo.
  5. 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?
  6. 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.
  7. 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?
  8. 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érioPontos
O fluxo completo em cinco etapas, com o que prova e o que não prova2 pontos
O tempo de cada etapa medido, com o total e a etapa de maior custo2 pontos
As cinco etapas do sketch copiadas, com os tempos e a contagem final2 pontos
A regressão de propósito, com o resultado da suíte antes e depois2 pontos
Os dois resultados da execução seguidas, e a resposta sobre o que variou2 pontos
Os pares de testes que dependem um do outro, listados1 ponto
A suíte rodada fora de ordem, com o resultado anotado1 ponto

Erros comuns

ErroComo apareceCorreçã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 ordemO 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 sistemaA 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 lerA 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 salvamentoOito 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 defeitoO 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.