Wrangling Two Monsters Part II: TDD with PICO C/C++ SDK in a Nutshell

Intro

In part I, we’ve addressed the first monter, setting up a CMake project for TDD with Pico C/C++ SDK. This article will attempt to wrangle the second monster, the PICO C/C++ SDK itself

TDD Setup Overview

TDD Overview

Test driven development is a software engineering methodology where a failing test is written before the production code. Then just enough production code is written for the test to pass, followed by clean up/refactoring. This development cycle is called RED/GREEN/REFACTOR. The description above is an oversimplification of the process, but it will server it’s purpose for this article.

A Short TDD Example

As per Pico C/C++ SDK examples, before Pico board can blink an LED, a basic pin setup must be performed. First a GPIO pin must be selected (initialized) for the task. Second, the same pin must be configured as an output pin.

GPIO Initialization Test (RED)

Check out gpio_init_test branch if you are following along:

git checkout gpio_init_test

Let’s start simple. In order to keep the code modular and therefore testable lets move the application source code in src/app.c and Corresponding headers to src/include/app.h. The program entry point will be app_main() defined in the src/App.c file. It will, in turn, setup GPIO pins, and later perform the blinking.

//App.c:

Void app_main(){
  //initialize GPIO
  //then blink
}

Following the TDD RED/GREEN/REFACTOR methodology we’ll start by writing a failing test which expects app_main() to call gpio_init() with The correct parameters.

First we’ll create a test group which will manage the desired state for our tests. For now it will be limited only to cleanup and expectation checks performed by the teardown() method.

TEST_GROUP(App)
{
    void setup(){
      //TODO
    }

    void teardown(){
      mock().checkExpectations();
      mock().clear();;
    }
};

With App test group setup up, the first test CallsGPIOInitWithCorrectArguments will look like this:

TEST(App, CallsGPIOInitWithCorrectArguments){
  unsigned int led_pin = 15;

  mock().expectOneCall("gpio_init")
    .withUnsignedIntParameter("pinNum", led_pin);

  app_main();
}

The biggest bottleneck/sticking point of doing TDD is the collaborators (dependencies like I/O, OS, hardware, etc). Often it may be impossible to test against a collaborator directly, in which case those dependencies must be substituted while allowing to validate the behavior of code under test (CUT).

In the test above, we will use CppUTest Mock framework. Mock frameworks allow to validate that code under test will invoke dependent on code (DOC) a specific amount of times, in a specific order, with specific arguments.

Outlining the exact way that the code under test must invoke the dependent on code is called recording the expectations.

In the test above the we expect gpio_init() to be called exactly once, with a single unsigned integer argument of 15.

The last part of the test will call the app_main() function which is responsible for selecting the correct GPIO pin by calling gpio_init().

Since there are no GPIO pins to configure on the development workstation (host target), we must mock the actual call to the gpio_init() function. Since we test C code from the C++ framework (CppUTest), the syntax will vary slightly.

void gpio_init(uint pinNum){
  mock_c()->actualCall("gpio_init")
    ->withUnsignedIntParameters("pinNum", pinNum);
}

Once the test is written we’ll complete the RED phase of TDD cycle by compiling and running a failing test.

cmake -B build_host -DPICO_PLATFORM=host
cmake --build build_host/ -t appTestRunner
./build_host/test/appTestRunner 

The first command will initialize CMake build system for the host (dev laptop) target, if not already done so. The second command will perform the build of the test runner binary and the code under test (appTestRunner CMake target). The last command will run the appTestRunner binary.

The results should look like the screenshot below:

failing test

GPIO Initialization Implementation (GREEN)

Check out gpio_init_minimal_impl branch if following along.

git checkout gpio_init_minimal_impl

Once we have a failing test that properly compiles and runs, it is time to write the simplest app_main() implementation needed to pass the test.

//App.c

void app_main(void){
  gpio_init(15); 
  //TODO blink;
}

Then recompile and rerun the tests.

cmake --build build_host/ -t appTestRunner
./build_host/test/appTestRunner 

The results should look as follows:

passing test

Clean Up (REFACTOR)

Check out gpio_init_refactor branch if following along.

git checkout gpio_init_refactor

While looking at the minimal implementation of app_main() one may note that there is a magic number hiding in it. Once the tests are passing and code under test behaves as expected it is time to clean up. This is the exact purpose of the REFACTOR phase of the TDD cycle. Since the GPIO number is not going to change over the course of the program execution, it makes sense to replace it with a macro defined in App.h. Of course any code modification will be followed by rebuilding and rerunning of the tests to ensure that the code under test still behaves as expected.

If you are following along, see the code changes by running:

git diff 62fedd3455f7eef3a0c45e9d4dca89c976812463

The results should look as follows:

refactor diff

This is TDD with Pico C/C++ SDK in a nutshell. The test harness (CppUTest) provides a safety net for refactoring the code, while ensuring that the changes did not break anything. However, refactoring does not mean changing functionality or adding features. More on that later…

But wait… you can’t blink an LED with what we have so far…

More to follow…

PS if you are impatient see blog_blink branch.

Tags: