ÍNDICE DE CONTEÚDO
Introdução
Sempre que vou discutir o uso de testes unitários em sistemas embarcados é recorrente a pergunta: Mas como você faz com o hardware?
Bem, a questão sempre aparece, é importante, mas no fim das contas a questão deveria ser: Quando inserir o hardware no processo de testes?
Para responder a essa questão temos de considerar primeiro a razão de existir daquele teste. A maior parte dos testes que escrevo são testes que existem para validar que a lógica da execução de uma parte do sistema está correta. Queremos portanto validar que para um dado cenário de execução da função, ou seja, para uma situação em que as variáveis das quais a função depende estejam em um valor especificado pelo teste, ações serão tomadas corretamente, e a saída da função será a esperada.
Em sistemas embarcados muitas vezes o problema reside na utilização de um periférico do microcontrolador que não está presente na máquina de desenvolvimento, então não é possível testar aquele código, certo? Errado!
Embarcados Experience 2024: Evento Presencial
Participe do Embarcados Experience 2024 em São Paulo. Conhecimento técnico, palestras, workshops e oportunidade de networking com profissionais experientes.
Primeiro o objetivo do seu teste não deveria ser: O SPI está funcionando, mas sim, estou mandando as informações corretas ao SPI. O primeiro teste é sim relevante e só pode ocorrer com o uso do dispositivo e no hardware em questão. Entretanto, o segundo cenário é igualmente relevante e muito frequente.
Motivos para usar um substituto no seu teste
Antes de chegar ao uso das bibliotecas que pretendo apresentar neste texto, quero apontar alguns motivos principais para o uso de um substituto:
1 – É uma dependência que não faz parte do objetivo do meu teste.
Quando estamos escrevendo um teste para um dado subsistema, temos a tendência de querer chamar todas as suas dependências. Isso não é necessário em todos os estágios. Vamos considerar o exemplo de uma aplicação em IoT que faz uso de um modem 3G para a comunicação. Se vamos testar a montagem e envio das requisições precisamos do código referente ao modem, correto? Errado! O que precisamos é determinar como será a interface para o código cliente que utilizará o modem e definir o “contrato” de utilização. Naturalmente é importante ter testes que integrem as duas soluções de fato, mas esse é um outro teste em uma outra fase. Um passo de cada vez.
2 – É uma dependência que é impossível de ser integrada no ambiente de desenvolvimento.
Esse é o caso típico de quando vamos acessar um periférico em um microcontrolador. O endereço de memória em que ele está mapeado não pode ser acessado no ambiente de desenvolvimento pois não há o periférico por lá.
3 – Existe um tempo razoável para o uso do recurso.
Consideremos novamente a aplicação IoT que deve trocar mensagem com um sistema na nuvem. Pro seu código, que vai tratar a informação, não é relevante saber que a conexão foi fechada via TLS e o servidor respondeu em um determinado tempo. O que é relevante é: Enviei a requisição X, recebi a resposta Y e tomei a ação Z.
Resumindo e voltando a questão do objetivo do teste: Queremos validar o comportamento do código em teste.
Um caso de teste: Aguardando um semáforo e enviando informação via SPI.
Para mostrar situações que são típicas ao escrever testes em sistemas embarcados vamos tomar como exemplo uma aplicação que aguarda um semáforo para enviar via SPI um valor, e em caso de timeout envia um valor diferente. É um caso de teste simples mas nos permite propor algumas ideias:
- Como lidar com o loop infinito que aparece nas funções que executam threads em RTOS?
- Como controlar uma dependência que faz parte do meu teste para gerar casos de teste?
- Como tratar o acesso ao hardware?
Uma pausa para escrita do teste: Catch
Para a escrita do teste vamos usar a ferramenta chamada Catch. Há outros frameworks de teste disponíveis e você pode experimentar para encontrar aquele que lhe agrade mais. O nosso primeiro passo é descrever o comportamento do nosso sistema.
1 2 3 4 5 6 7 8 9 10 |
SCENARIO("Uma informação deve ser enviada via SPI"){ GIVEN("A requisição de envio virá antes do timeout"){ THEN("O sistema enviará o valor 0xCA") { } } GIVEN("Haverá o timeout antes da requisição de envio"){ THEN("O sistema enviará o valor 0xFE") { } } } |
Usando o Fake Function Framework
Para completar o nosso teste vamos utilizar uma biblioteca chamada Fake Function Framework (FFF). Com ela iremos substitur as funções de dependências no momento do link. Essa é uma boa técnica para substituir funções que atuam diretamente com o hardware e funções de sistema e dependências que não farão parte do nosso conjunto de testes. A implementação dessas funções não entrará na compilação do teste. E então proveremos uma implementação usando o FFF.
Para a implementação vamos supor a utilização do CMSIS-RTOSv2.
Observe no quadro anterior que o teste que descrevemos aguarda uma requisição de envio. Vamos então supor que a função é uma task em um RTOS e o que aguardaremos é a disponibilidade de um semáforo.
Vamos complementar o teste:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include "catch.hpp" extern "C" { #include "fff.h" #include "cmsis_os2.h" #include "spi.h" } FAKE_VALUE_FUNC(osStatus_t, osSemaphoreAcquire, osSemaphoreId_t, uint32_t); FAKE_VOID_FUNC(spi_send, uint8_t); SCENARIO("Uma informação deve ser enviada via SPI"){ GIVEN("A requisição de envio virá antes do timeout"){ RESET_FAKE(osSemaphoreAcquire); RESET_FAKE(spi_send); osSemaphoreAcquire_fake.return_val = osOK; /* Chamada da função sob teste, para integração com o RTOS a assinatura * da função deve ser diferente. Para manter o foco na questão do mock e * do teste materei assim. */ infoTask(); THEN("O sistema enviará o valor 0xCA") { REQUIRE(spi_send_fake.call_count == 1); REQUIRE(spi_send_fake.arg0_val == 0xCA); } } GIVEN("Haverá o timeout antes da requisição de envio"){ RESET_FAKE(osSemaphoreAcquire); RESET_FAKE(spi_send); osSemaphoreAcquire_fake.return_val = osErrorTimeout; infoTask(); THEN("O sistema enviará o valor 0xFE") { REQUIRE(spi_send_fake.call_count == 1); REQUIRE(spi_send_fake.arg0_val == 0xFE); } } } |
Alguns pontos a destacar:
- O início do teste é dedicado à construção do cenário, com estado das respostas e opções do teste;
- A função sob teste é exercitada como uma chamada de função normal dentro da estrutura do teste;
- Após a execução dos testes, os resultados são verificados com o auxílio dos substitutos construídos, usando o REQUIRE no nosso caso.
Conclusão
Executar testes em sistemas embarcados envolve várias etapas com diversos objetivos diferentes. Para o modelo de teste que apontei neste artigo, o que objetivamos é verificar se elementos da lógiga de negócios do sistema estão sendo respeitados. Validamos que a partir de uma entrada todas as dependências são chamadas na forma correta e um número certo de vezes, e assumimos o controle dessas dependências para validar que dado um certo cenário teremos a resposta correta do código implementado.
Show o artigo! Parabéns!
Euripedes, que legal! Estava procurando algo semelhante duas semanas atrás. Acho que seu artigo vai nos ajudar. Obrigado e parabéns!