Pular para conteúdo

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:

class ISensor {
public:
    virtual double leitura() const = 0;
    virtual ~ISensor() {}
};

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, com assert(), 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.ino deve 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.