Testes unitários no ESP-IDF com GoogleTest no host

Debug via log e flash: o ciclo que todo fev ESP-IDF conhece

Embora eu não seja profissional da área e minha formação esteja longe de ter relação com o tema, eu gosto de programar como hobby e necessidade. Estava fazendo uma automação para o sítio usando ESP32 e me deparei com um problema que a maioria das pessoas passa: encher o código de logs unicamente para debug, dar flash, monitorar, alterar uma pequena coisa, novo flash, e o ciclo se repete, o famoso flash and pray. Embora isso funcione — e o log do IDF seja bom, pois podemos setar o nível cada log e depois ocultá-los em produção —, a tarefa se torna cansativa e demorada, testar todos os aspectos de um componente um pouco mais complexo levaria muito tempo.

Foi então que eu me deparei com os testes unitários, unit tests, que é uma forma de checar que a menor parte testável do código, geralmente uma função ou método, funciona de forma isolada nos mais variados cenários. Em uma analogia com hardware, seria como verificar que cada componente funciona antes de soldá-lo na placa, alguns componentes precisam de outros para funcionar, e podem funcionar de forma distinta dependendo do valores dos componentes extras, por ex., um regulador de tensão LM317 varia sua tensão de saída de acordo com o valor dos resistores extras que ele precisa para funcionar.

O IDF, SDK oficial da Espressif para suas placas da família ESP32 já vem com o Unity como um componente, um framework de testes que funciona bem para testes unitários na própria placa, no device, mas ainda há a necessidade da placa e de dar flash e monitorar. É mais rápido que logs e flash, pois podemos criar vários test cases, explorando todas as possibilidades, mas ainda temos que dar flash na placa e monitorar o resultado dos testes. A documentação oficial tem uma seção sobre Unit Testing in ESP32.

Host-based testing: rodando testes no Linux sem precisar da placa

O IDF também permite rodar testes em host linux, ou seja, na própria máquina que está sendo usada para programar. Isso é configurado setando o target em linux, comando idf.py –preview set-target linux. Embora o target seja “linux”, parece que ele roda em Macos com algumas limitações, conforme a segunda parte desse video: QEMU e Linux Targets

As vantagens de rodar testes no host são a velocidade de compilação, quase sempre é mais rápida. Não ter de esperar dar flash e rodar na placa, logo após a compilação o binário *.elf está pronto para ser executado na própria máquina. Por fim a facilidade de CI, cada push pode rodar os testes com GitHub Actions, por ex, e validar o código, já que não precisa da placa.

GoogleTest ou Unity? O caso dos mocks em C++

Quando estamos desenvolvendo um componente mais complexo em C++, é importante que ele tenha classes com distintas responsabilidades, todas baseadas em interfaces (classe abstrata com métodos virtuais puros), para que ele seja facilmente testável: criamos mocks a partir da interface, o mock é um objeto que simula o comportamento de um objeto real, mas onde podemos simular vários cenários e definir exatamente o que o mock deve fazer: retornar falha, retornar esse ou aquele valor etc. 

Imagine que você tem um componente que lê a distancia através de um sensor ultrassônico, com um mock da classe que faz a leitura do sensor você consegue simular tudo que quiser: qualquer distância ou tempo de resposta, timeout, leituras válidas, leituras fora dos valores mínimo e máximo, e verificar se o componente retorna exatamente aquilo que você previu em cada caso. Sem placa, sem esperar flash e monitorar a saída.

Embora o Unity seja nativo no IDF e funcione bem para mockar os próprios componentes do IDF via CMock, ele não lida bem com classes em C++, é nesse momento que o GoogleTest (GTest) brilha, o GoogleMock (GMock), que vem junto com o GTest, permite que os mock sejam escritos em poucas linhas, dentro do arquivo de testes mesmo. 

Componentes no ESP-IDF: como o build system enxerga seu código

No ESP-IDF, cada módulo ou biblioteca é um componente: uma pasta com seu próprio CmakeLists.txt que registra seus SRCS, INCLUDE_DIRS e dependências. Quando um componente precisa de outro, ele declara suas dependências no REQUIRES ou PRIV_REQUIRES do CmakeLists. Mais informação pode ser encontrada na documentação: Build System e nesse excelente vídeo: Mastering ESP-IDF Build System.

Esse artigo foi feito em cima de um tutorial que eu fiz e está disponível no repositório: GoogleTest with ESP-IDF. Nesse artigo cubro a primeira partes do tutorial: 01_basic_test. Conforme for citando os arquivos, vou linkando diretamente aos arquivos do repositório. 

Nesse exemplo estamos testando um componente, e temos a seguinte estrutura de arquivos:

01_basic_test/		# Root folder and component name
├── CMakeLists.txt      # Component registration
├── include/
   ├── i_sum.hpp       # Interface (abstract base class)
   └── sum.hpp         # Concrete class header
├── src/
   └── sum.cpp         # Implementation
├── host_test/
   ├── gtest/          # GTest wrapper component
   └── test_sum/       # Test project for the Sum class
└── test_apps/
    └── test_build/     # Project to verify ESP32 compilation

O arquivo include/i_sum.hpp merece destaque: embora neste estágio inicial a classe Sum seja simples o suficiente para ser testada sem uma interface, já a introduzimos pensando no crescimento do componente. Quando o sistema evoluir e mais classes surgirem, poderemos testar outras classes usando um mock da classe Sum derivado dessa interface, garantindo o isolamento típico de testes unitários.

O diretório host_test/test_sum/ é um projeto standalone de testes, com sua própria estrutura completa: possui um CMakeLists.txt na raiz e uma pasta main (que é um componente) com seu próprio CMakeLists.txt para registrar o componente principal de teste. Já o src/ contém a implementação concreta atual, e o test_apps/test_build/ é um projeto separado usado apenas para validar a compilação do componente para o target ESP32, servindo como ponte para futuros testes em hardware.

O wrapper do GTest: FetchContent, linux guard, build phase guard, INTERFACE

O Google Test (GTest) não faz parte do framework ESP-IDF. Para utilizá-lo em testes de unidade no host (Linux), é necessário criar um componente wrapper que o integre ao sistema de build. Neste exemplo, esse wrapper está localizado em host_test/gtest/ e consiste unicamente em um arquivo CmakeLists.txt, não possui arquivos de código-fonte. Ele funciona como uma ponte, baixando o GTest e o expondo aos componentes de teste de forma controlada.

Linux guard

Como o GTest é executado no host (e não no microcontrolador), é essencial que ele seja incluído apenas quando o alvo de build for o Linux. O wrapper verifica a variável IDF_TARGET para garantir isso:

idf_build_get_property(target IDF_TARGET)
if(NOT ${target} STREQUAL "linux")
    return()
endif()

Isso impede que o código do GTest seja acidentalmente compilado para o alvo esp32, o que poderia causar erros e aumentaria o tamanho do binário final desnecessariamente.

FetchContent

O primeiro passo é obter o código do GTest. O wrapper utiliza o módulo FetchContent do CMake para baixar a biblioteca diretamente do GitHub durante o processo de build.

# Exemplo do CMakeLists.txt do wrapper
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        v1.14.0  # Versão especificada
)

Essa abordagem mantém o repositório do projeto enxuto, pois não é necessário versionar o código do GTest, e a atualização da versão é feita simplesmente alterando a GIT_TAG.

Build phase guard

O sistema de build do ESP-IDF opera em duas fases principais: uma fase de inicial (para mapear componentes e dependências) e a fase de build propriamente dita. O comando FetchContent_MakeAvailable não pode ser executado na primeira fase, pois tentaria baixar código de uma fonte externa enquanto o sistema ainda está entendendo a estrutura do projeto. Por isso, o wrapper adiciona um guarda para garantir que o download e a disponibilização do GTest ocorram apenas na fase correta de build.

if(NOT CMAKE_BUILD_EARLY_EXPANSION)
  FetchContent_MakeAvailable(googletest)
endif()

INTERFACElinking

O wrapper não compila nenhum código próprio, sua única função é disponibilizar o GTest para outros componentes. Por essa razão, ele é registrado como um componente do tipo INTERFACE no CMakeLists.txt que o declara (no diretório host_test/gtest/). Isso significa que ele não produz uma biblioteca com código objeto, mas apenas expõe GTest e GMock para quem precisar deles. Qualquer componente de teste que precise usar o GTest basta listar gtest em seus REQUIRES (ou PRIV_REQUIRES) para ganhar acesso automático aos cabeçalhos e bibliotecas do Google Test e Google Mock.

Essa estrutura de wrapper, embora criada para um único projeto de teste neste exemplo, estabelece um padrão limpo e reutilizável que pode ser compartilhado por múltiplos projetos de teste dentro da pasta host_test/.

Primeiros testes: smoke, basic addition, constrained

Os testes para o componente Sum são organizados em três grupos com objetivos distintos, garantindo desde a validação do ambiente até a verificação de regras de negócio. Os testes que estão no arquivo: /host_test/test_sum/main/test_sum.cpp.

Smoke test

É o teste mais simples possível. Sua única função é validar que o GoogleTest está configurado corretamente e sendo executado. Se este teste falhar, o problema está na infraestrutura de testes, não no código do componente.

TEST(TestSum, GTestSmokeTest) {
    EXPECT_TRUE(true); // Deve sempre passar se o ambiente estiver OK
}

Basic addition

Este grupo cobre a funcionalidade central do método int add(a, b), testando seu comportamento com diferentes tipos de entrada para garantir que a operação matemática básica funcione em todos os casos esperados. 

  • Números positivos: TEST(TestSum, BasicAddition) 
  • Números negativos: TEST(TestSum, NegativeAddition)

Constrained addition

Foca no método int add_constrained(a, b), que possui uma regra de negócio: deve retornar -1 se o resultado da soma exceder 10 ou se algum dos parâmetros não estiver entre 0 e 10. Os testes exploram três cenários críticos:

  • Caminho feliz: TEST(TestSum, AddConstrained_HappyPath), entradas que resultam em uma soma válida dentro do limite
  • Casos de borda: TEST(TestSum, AddConstrained_EdgeCases), testa exatamente os valores nos limites da regra, como 0 + 0 = 0; 5 + 5 = 10 e 0 + 10 = 10. 
  • Fora dos limites: TEST(TestSum, AddConstrained_OutOfRange), testa casos em que os parâmetros ou o resultado estão fora do range: 11 + 0, -1 + 5 e 6 + 5.

Rodando os testes:

cd /host_test/test_sum		  	# Ir até a pasta
idf.py --preview set-target linux	# Setar o target
idf.py build				   	# Compilar
./build/test_sum.elf  		   	# Rodar o executável

Testes Unitários ESP-IDF: GoogleTest no Linux
Figura 1 – Testes rodando  em host

Considerações Finais

Os testes em host não são substituem 100% dos testes feitos no device, na placa. No final das contas é preciso fazer o flash na placa e observar como seu código se comporta, principalmente quando é um hardware próprio, mas com uma boa bateria de unit tests em host, você consegue detectar erros bem antes de subir o código na placa. Os testes finais na placa, de integração, são bem mais direcionados, com o foco em testar situações próprias do hardware.

Depois de toda essa conversa sobre ciclo de debug, testes unitários e a facilidade de rodar tudo no host com GTest, qual tem sido a sua experiência com tudo isso? Você ainda vive no ciclo “flash and pray” ou já adotou alguma estratégia de testes? Já chegou a brincar com o target linux do IDF ou continua usando só a placa mesmo? Conta aí nos comentários quais métodos você usa para garantir que seu código funcione antes de colocar em produção.

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Comentários:
Notificações
Notificar
0 Comentários
recentes
antigos mais votados
Inline Feedbacks
View all comments
Home » Software » Testes unitários no ESP-IDF com GoogleTest no host

EM DESTAQUE

WEBINARS

VEJA TAMBÉM

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Talvez você goste: