TDD - You can write code without actual hardware (Part I)

TDD - You can write code without actual hardware (Part I)

This post is an extension of "Another mock example using StubWithCallback". Previously, test_eeprom_controller.c includes only one test case.

void test_mock_write_read_byte(void)
{
    uint32_t test_address = 0;
    uint8_t write_data = 0xAA;
    flash_write(test_address, &write_data, 1);

    uint8_t read_data = 0;
    flash_read(test_address, &read_data, 1);
    TEST_ASSERT_EQUAL(write_data, read_data);
}

This post will add more test cases with more production code in eeprom_controller.c. We would like to read one byte from the given EEPROM address and also write one byte of data to the given EEPROM address and verify with unit testing.

From setUp function, we initialize mock_eeprom values to be all 0xFF:

void setUp(void)
{
    uint16_t i = 0;
    // initialize mock eeprom
    for(i = 0; i < EEPROM_SIZE; i++)
    {
        mock_eeprom[i] = 0xFF;
    }

    flash_read_StubWithCallback(mock_read);
    flash_write_StubWithCallback(mock_write);
}

We can read all data to verify 0xFF in test_eeprom_controller_read_byte:

void test_eeprom_controller_read_byte(void)
{
    uint16_t i;//make sure this is uint16_t not uint8_t due to EEPROM_SIZE is greater than 255
    uint8_t read_data;
    for(i = 0; i < EEPROM_SIZE; i++)
    {
        TEST_ASSERT_EQUAL(true, eeprom_read_byte(i, &read_data));//read operation returns success
        TEST_ASSERT_EQUAL(0xFF, read_data);//read data should be 0xFF from setUp
    }
}

To satisfy this code, we can start to write eeprom_read_byte in eeprom_controller.c like this:

bool eeprom_read_byte(uint32_t address, uint8_t* r_data)
{
    bool op_status = true;
    *r_data = 0xFF;

    return op_status;
}

Make sure to add a function prototype in eeprom_controller.h. I know this implementation is wrong since read data will be always 0xFF and op_status is always true. However, it will pass the current test case. This is the first step of TDD. We need to think about what other test cases should be added to refactor the production code.

The next thing to add is the given address's boundary check. First parameter cannot be over EEPROM_SIZE. Also, second parameter cannot be NULL(0). Let's add new test cases to validate the parameters.

void test_eeprom_controller_read_byte(void)
{
    uint16_t i;
    uint8_t read_data;
    for(i = 0; i < EEPROM_SIZE; i++)
    {
        TEST_ASSERT_EQUAL(true, eeprom_read_byte(i, &read_data));
        TEST_ASSERT_EQUAL(0xFF, read_data);
    }
    //New test cases!
    TEST_ASSERT_EQUAL(false, eeprom_read_byte(EEPROM_SIZE, &read_data));//checking the first parameter
    TEST_ASSERT_EQUAL(false, eeprom_read_byte(0, NULL));//checking the second parameter
}

TDD pattern is that the test should be always ahead and then, implement code based on unit test failure results. We can see failure now because the current production code always returns true and the test expects to see false. Let's refactor!

bool eeprom_read_byte(uint32_t address, uint8_t* r_data)
{
    bool op_status = false;//init as false
    *r_data = 0xFF;
    //add parameter check
    if(r_data != 0 && address < EEPROM_SIZE)
    {
        op_status = true;
    }

    return op_status;
}

Yeah! finally, test_eeprom_controller_read_byte is passed. Are we happy now? Unfortunately, we still have hard-coded read data. A current test case can pass but if any byte of EEPROM has been updated differently, the test would fail. Let's refactor again!

bool eeprom_read_byte(uint32_t address, uint8_t* r_data)
{
    bool op_status = false;
    if(r_data != 0 && address < EEPROM_SIZE)
    {
        flash_read(address, r_data, 1);
        op_status = true;
    }

    return op_status;
}

This is the time to bring interface_flash.c's function! We know when flash_read is called in the unit test, it will be substituted by mock_read which is defined in test_eeprom_controller.c.

uint8_t mock_eeprom[EEPROM_SIZE];

static void mock_read(uint32_t memory_address, uint8_t* r_buffer, uint16_t length, int num_calls)
{
    uint16_t i;
    uint8_t* copy_ptr = r_buffer;
    for(i = 0; i < length; i++, copy_ptr++)
    {
        *copy_ptr = mock_eeprom[memory_address];
    }
}

Our goal is to implement eeprom_controller.c not interface_flash.c because the interface can be varied by the vendor's driver code. That part will be hardware-specific code.

The next article will be continuously adding a test case for write byte and verifying with the read function we just implemented here.

Did you find this article valuable?

Support Hyunwoo Choi by becoming a sponsor. Any amount is appreciated!