When developing embedded C applications for 8-bit AVR microcontrollers, I found the lack of a unit test framework troublesome. Especially as unit testing now forms a standard part of our C-sharp and Java development processes.

There are a number of C and C++ unit testing frameworks available but nothing geared up for running in the target environment of an 8-bit AVR.

The “framework” produced here is a lightweight starting point that can be enhanced to provide output in any form useful to the user. It is currently used in the AVR Simulator as part of the AVR Studio 5 environment, but there’s no reason why it couldn’t be run on the target device with test result output streamed over a USART connection.

Init levels

Adding the tests in JUnit etc allows you to scan the classes and run them all automatically. We can’t quite do the same in C. However, what we can do is to make use of the .init levels and have a function for each test automatically called on system start-up. These functions will then automatically add each test to the test suite.

AVR Memory Sections

Simulator

By running the tests in the AVR simulator, we can use the Watch tools to view the test results by putting breakpoints after each test is run, or after all tests have run. Monitor the m_Test_activeTest for the current test data, and m_Test_result for the final results.

The Code

test.h

#ifndef TEST_H
#define TEST_H

#include <inttypes.h>

// typedefs
typedef enum TestResult
{
    NOT_RUN = 0,
    SUCCESS,
    FAILURE
} TestResult;

typedef struct Test_TestHolder
{
    char * name;
    void (*testFunction)(void);
    char * file;

    int line;
    TestResult testResult;

    struct Test_TestHolder* next;
} Test_TestHolder;

// Initialise the test framework

void Test_add(Test_TestHolder* test);

void Test_assertTrueLog(uint8_t condition, uint16_t lineNumber);

void Test_assertEqualLog(uint16_t expected, uint16_t actual,
uint16_t lineNumber);

void Test_runall(void);

#if __TEST__

void Test_init(void) __attribute__ ((naked))
    __attribute__ ((section (".init7")));

#define Test_run() {Test_runall(); for(;;); }

#define Test_test(MODULE, NAME)
	    void MODULE##_test_##NAME(void);
	    void MODULE##_appendtest_##NAME(void) 
            __attribute__ ((naked))   
            __attribute__ ((section (".init8")));
	    Test_TestHolder m_##MODULE_test_##NAME = { #NAME,
            MODULE##_test_##NAME, __FILE__, 0, NOT_RUN };
	    void MODULE##_appendtest_##NAME(void) { 
            Test_add( &m_##MODULE_test_##NAME ); }; 
	    void MODULE##_test_##NAME(void)

#else

void Test_init(void);

#define Test_run()

#define Test_test(MODULE, NAME)
	    void MODULE##_test_##NAME(void);
	    void MODULE##_test_##NAME(void)

#endif

#define Test_assertTrue(condition)
    Test_assertTrueLog((condition), __LINE__);
    if (!(condition)) {
        return;
    }		

#define Test_assertEquals(expected, actual)
    Test_assertEqualLog((expected), (actual), __LINE__);
    if ((expected) != (actual)) {
        return;
    }

#endif /* TEST_H */

test.c

#include "test.h"

#include <stdlib.h>
#include <string.h>

Test_TestHolder* m_Test_head;

Test_TestHolder* m_Test_activeTest;

typedef struct Test_ResultType
{
    uint16_t totalTests;
    uint16_t successCount;
    uint16_t failureCount;
} Test_ResultType;

Test_ResultType m_Test_result;

// Initialise the test framework
void Test_init(void)
{
    m_Test_head = NULL;
    m_Test_activeTest = NULL;

    m_Test_result.totalTests = 0;
    m_Test_result.successCount = 0;
    m_Test_result.failureCount = 0;
}

void Test_add(Test_TestHolder* test)
{
    // Put to front of chain
    test->next = m_Test_head;
    m_Test_head = test;
}

void Test_assertTrueLog(uint8_t condition, uint16_t lineNumber)
{
    // We have the active test
    if( !(condition) )
    {
        m_Test_activeTest->testResult = FAILURE;
        m_Test_activeTest->line = lineNumber;
    }
}

void Test_assertEqualLog(uint16_t expected, uint16_t actual,
    uint16_t lineNumber)
{
    if( expected != actual )
    {
        m_Test_activeTest->testResult = FAILURE;
        m_Test_activeTest->line = lineNumber;
    }
}

// Run through all the tests
void Test_runall(void)
{
    // Reset counts
    m_Test_result.totalTests = 0;
    m_Test_result.successCount = 0;
    m_Test_result.failureCount = 0;

    // Reset status of all
    m_Test_activeTest = m_Test_head;
    while( m_Test_activeTest != NULL )
    {
        m_Test_result.totalTests++;

        m_Test_activeTest->testResult = NOT_RUN;
        m_Test_activeTest->line = 0;

        // next in the chain
        m_Test_activeTest = m_Test_activeTest->next;
    }

    // Now execute the tests
    m_Test_activeTest = m_Test_head;
    while( m_Test_activeTest != NULL )
    {
        m_Test_activeTest->testFunction();

        if( m_Test_activeTest->testResult == NOT_RUN )
        {
            m_Test_activeTest->testResult = SUCCESS;
            m_Test_result.successCount++;
        }
        else
        {
            m_Test_result.failureCount++;
        }

        asm("nop");

        // next in the chain
        m_Test_activeTest = m_Test_activeTest->next;
    }

    // Get the results
    asm("nop");
}

// Examples
/*
Test_test(Test, testWillFail)
{
    Test_assertEquals(1, 0);
}

Test_test(Test, testWillPass)
{
    Test_assertTrue(1);
}
*/

main.c

#include "test.h"

int main(void)
{
    Test_run();        // System code
}