Skip to content

BLOG

A Quick Guide to Unit Testing Hardware in Arduino

Testing is an important part of software development, and that goes doubly for projects as large and complex as an IoT product. But while testing a cloud back end or a mobile app may be a well-defined problem space with myriad tooling and best practices, testing the “T” in IoT can be intimidating— especially to those coming from a web background. Fortunately, good tests are still totally within reach when writing embedded firmware.

At Very, we use a variety of languages and ecosystems deliver IoT projects, but I want to focus on Arduino and C++ in particular. I’ve recently worked on firmware for a few IoT projects in Arduino and I’ve found some helpful techniques that help me realize all the benefits of a well-tested system.

Before I go into detail, at Very we use PlatformIO for many of our firmware projects, and PlatformIO comes with the C/C++ testing library Unity built in. Many of the code examples here will be using Unity as the test “driver.” Everything else will be Arduino-flavored C++.

Unit testing in Arduino is very straightforward, just like many other environments — even if you want to test something that might be foreign to another environment like controlling an LED:

#include <Arduino.h>
#include <unity.h>

uint8_t led_pin = 5;

void turn_led_on() {
    digitalWrite(led_pin, HIGH);
}

void test_led_state_high(void) {
    turn_led_on();
    TEST_ASSERT_EQUAL(HIGH, digitalRead(led_pin));
}

void setup() {
    // NOTE!!! Wait for >2 secs
    // if board doesn't support software reset via Serial.DTR/RTS
    delay(2000);

    UNITY_BEGIN();
    RUN_TEST(test_led_state_high);
    UNITY_END();
}

In the snippet above, Arduino supplies a method to send a digital signal to a pin that, in this example, is controlling whether an LED is on or off. The test then runs the turn_led_on function and checks if the side effect of the LED pin being in the HIGH state is true or not.

There are a few problems with this code:

First, it depends on the code running on some kind of hardware, whether that is a development chip or a full PCBA. As the comment in the first lines of the setup function indicates, running tests on real hardware may come with unexpected challenges.

While running the tests on hardware isn’t necessarily a bad idea, it can be slow to build, upload, and even run compared to the high-powered computer on which the software was likely written in the first place. As a developer practicing TDD, having a short feedback loop is critical, and needing to go through a deploy process just to run the tests will result in fewer and worse tests — and by extension, worse code. Leave running the tests on hardware for some CI process that can verify builds are working in an asynchronous manner.

Secondly, this code goes too far and relies on the implementation of an Arduino dependency, in this case, the digitalWrite function. Testing side effects can be risky business. What if the digitalWrite function were to fail for some reason? The test would fail as well, even though the code executed exactly as anticipated.

Anyone familiar with common testing patterns will be able to guess what can be used to fix these problems: mocks! But how do mocks work in the Arduino environment? The Unity library doesn’t have inherent mocking capability, but fortunately it is possible to create mocks based off of C++ Abstract Classes:

#include <Arduino.h>

class PinInterface {
public:
  virtual void doDigitalWrite(uint8_t pin, uint8_t val);
};

class Pin: public PinInterface {
public:
  void doDigitalWrite(uint8_t pin, uint8_t val) {
    digitalWrite(pin, val);
  }
};

The first class above is the abstract class that defines a pure virtual function. This class will act as a sort of interface when it is used as the base class for another, more functional class. The Pin class uses the PinInterface and defines a function that matches the signature of the virtual private function defined in the interface, and it simply calls the Arduino-supplied digitalWrite function. Now, using dependency injection, this Pin class can be included in other classes that need to perform digital write operations on pins. However, I’m only halfway to being able to successfully test the original function. Now for the Mock:

#include <PinInterface.h>
#include <unity.h>

class MockPin : public PinInterface {
public:
  uint8_t pinNumber = 0;
  uint8_t pinVal = 0;

  void doDigitalWrite(uint8_t pin, uint8_t val) {
      pinNumber = pin;
      pinVal = val;
  }
};

uint8_t led_pin = 5;
MockPin pin;

void turn_led_on() {
    pin->doDigitalWrite(led_pin, HIGH);
}

void test_led_state_high(void) {
    turn_led_on();
    TEST_ASSERT_EQUAL(HIGH, pin.pinNumber);
    TEST_ASSERT_EQUAL(HIGH, pin.pinVal);
}

void setup() {
    UNITY_BEGIN();
    RUN_TEST(test_led_state_high);
    UNITY_END();
}

Pretty cool. Now it is possible to define a simple mock class and for the test to check for exactly what is expected rather than relying on some hardware implementation. It is even possible to run this code without deploying it to hardware first since it doesn’t depend on any hardware-specific code.

So remember: try to avoid tying your code, and more specifically, your tests, to hardware and use common test patterns when it makes sense.

Looking for an IoT development team that cares about unit testing? Tell us more about your next IoT project today.