15. Arquitetura testável (interfaces e mocks)¶
Este capítulo fecha o círculo entre tudo que o curso ensinou: usa polimorfismo (Unidade 5) para desacoplar lógica de hardware, e usa exatamente o mesmo assert() das listas para testar código que, no fim, vai controlar um LED de verdade — sem nunca precisar abrir o Wokwi para isso.
O problema: código que só roda com hardware ligado¶
Se você escrever a lógica de decisão misturada com chamadas a analogRead/digitalWrite, só existe um jeito de testar essa lógica: rodar no hardware (ou no simulador) e observar. Isso é lento, difícil de automatizar, e praticamente impossível de cobrir todos os casos (e se o sensor ler exatamente no limite? e se ler um valor absurdo, fora de qualquer faixa esperada?).
// Tudo misturado -- so testavel rodando de verdade no hardware
void loop() {
int leitura = analogRead(34);
double temperatura = (leitura / 4095.0) * 50.0;
if (temperatura >= 30.0) {
digitalWrite(2, HIGH);
} else if (temperatura <= 28.0) {
digitalWrite(2, LOW);
}
delay(500);
}
A solução: uma interface entre a lógica e o hardware¶
Esta é a mesma ideia do capítulo 7 (separar parsing de leitura de arquivo) e do capítulo 11 (classe abstrata como contrato) — aplicada agora à fronteira entre software e hardware. Veja o projetos/logica/isensor.h do esqueleto do curso:
Duas implementações:
SensorFalso (projetos/logica/sensor_falso.h) — não toca em hardware nenhum, só devolve um valor programado pelo teste:
class SensorFalso : public ISensor {
private:
double valor_;
public:
explicit SensorFalso(double valor_inicial = 0.0) : valor_(valor_inicial) {}
void define_leitura(double valor) { valor_ = valor; }
double leitura() const override { return valor_; }
};
SensorReal (projetos/esqueleto-wokwi/sensor_real.h) — lê de verdade um pino analógico, só compila com o framework Arduino:
class SensorReal : public ISensor {
private:
int pino_;
public:
explicit SensorReal(int pino) : pino_(pino) {}
double leitura() const override {
int bruto = analogRead(pino_);
return (bruto / 4095.0) * 50.0;
}
};
A lógica de decisão não conhece nenhuma das duas¶
ControladorTemperatura (projetos/logica/controlador_temperatura.h/.cpp) só conhece ISensor — a interface, nunca uma implementação concreta:
class ControladorTemperatura {
private:
const ISensor& sensor_;
double limite_;
bool ventoinha_ligada_;
public:
ControladorTemperatura(const ISensor& sensor, double limite)
: sensor_(sensor), limite_(limite), ventoinha_ligada_(false) {}
void atualiza(); // le sensor_.leitura(), decide ligar/desligar
bool ventoinha_ligada() const { return ventoinha_ligada_; }
};
Note a histerese implementada em atualiza() (margem de 2 graus entre ligar e desligar) — evita que a ventoinha fique oscilando ligado/desligado quando a leitura balança perto do limite. É um detalhe de lógica de controle, e é exatamente o tipo de regra que fica muito mais fácil de acertar (e de testar) fora do hardware.
Testando no host, sem Wokwi¶
projetos/esqueleto-host/teste_controlador.cpp usa SensorFalso para testar ControladorTemperatura com o mesmo assert() de todo o curso:
#include <cassert>
#include <iostream>
#include "controlador_temperatura.h"
#include "sensor_falso.h"
int main() {
SensorFalso sensor(20.0);
ControladorTemperatura controlador(sensor, /*limite=*/30.0);
controlador.atualiza();
assert(!(controlador.ventoinha_ligada()));
sensor.define_leitura(31.0);
controlador.atualiza();
assert(controlador.ventoinha_ligada());
// histerese: 29 graus ainda esta acima do limite - margem (28) -- mantem ligada
sensor.define_leitura(29.0);
controlador.atualiza();
assert(controlador.ventoinha_ligada());
std::cout << "Todos os testes passaram!\n";
return 0;
}
Rode com projetos/esqueleto-host/rodar.sh — nenhuma dependência de Arduino, Wokwi, ou hardware. Isso testa exatamente a mesma lógica que vai rodar no ESP32 de verdade, incluindo casos difíceis de forçar manualmente no simulador (como o comportamento exato no limite da histerese).
Ligando ao hardware de verdade: sketch.ino¶
O sketch.ino (projetos/esqueleto-wokwi/) é deliberadamente curto — ele só conecta pinos a SensorReal e chama ControladorTemperatura:
SensorReal sensor(PINO_SENSOR);
ControladorTemperatura controlador(sensor, LIMITE_TEMPERATURA);
void loop() {
controlador.atualiza();
digitalWrite(PINO_VENTOINHA, controlador.ventoinha_ligada() ? HIGH : LOW);
delay(500);
}
Nenhuma regra de negócio mora no sketch.ino — só fiação (pinos → objetos). Se um bug aparecer no comportamento da ventoinha, a primeira pergunta é "os testes de ControladorTemperatura continuam passando?" — se sim, o bug está na fiação (pino errado, sensor errado), não na lógica.
Máquina de estados como objeto¶
Sistemas embarcados frequentemente têm um conjunto de estados (por exemplo: Normal, Alerta, Critico) com regras de transição entre eles. Modelar isso como um enum class mais uma classe que decide as transições — em vez de uma pilha de if/else soltos no loop() — segue o mesmo princípio deste capítulo: a lógica de transição fica isolada e testável.
enum class Estado { Normal, Alerta, Critico };
class MaquinaDeEstados {
private:
Estado estado_atual_;
public:
MaquinaDeEstados() : estado_atual_(Estado::Normal) {}
void processa_temperatura(double temperatura) {
if (temperatura >= 40.0) {
estado_atual_ = Estado::Critico;
} else if (temperatura >= 30.0) {
estado_atual_ = Estado::Alerta;
} else {
estado_atual_ = Estado::Normal;
}
}
Estado estado_atual() const { return estado_atual_; }
};
Assim como ControladorTemperatura, essa classe não sabe nada de hardware — só de regras. Testável com SensorFalso (ou, aqui, chamando processa_temperatura diretamente com valores programados), e reaproveitável em qualquer projeto que precise de uma máquina de estados parecida.
O que o projeto final espera¶
Se você escolher o tema embarcado (recomendado), o projeto final espera exatamente esta arquitetura: pelo menos uma interface abstrata usada polimorficamente, com uma implementação real (Arduino) e uma "falsa" para teste, e a lógica de decisão testada no host antes de ir para o Wokwi. Comece a partir do esqueleto em projetos/ — ele já está pronto e testado; sua tarefa é estender, não recomeçar do zero.
Resumo¶
- Interface (
ISensor) separa "o que a lógica precisa saber" de "como o dado chega de verdade" — o mesmo padrão do capítulo 11, aplicado a hardware. - Implementação falsa (
SensorFalso) permite testar toda a lógica de decisão no host, comassert(), sem nunca precisar abrir o simulador. - Implementação real (
SensorReal) só existe no lado do Wokwi, porque só ela depende do framework Arduino. - O
sketch.inodeve ser fino — só fiação (pinos → objetos), zero regra de negócio. - Máquina de estados como classe/
enum class: mesma lógica de isolamento, aplicada a fluxos com múltiplos estados.