Search code examples
cunit-testingembeddedtddabstract-data-type

How to unit test your Abstract data type without getters and setters in C?


I am currently trying to refactor some of my embedded C code and am trying to apply what James W. Grenning is recommending in his Test-Driven Development for Embedded C book. For this, I am dividing my code in modules for each of my abstract data types, a bit like it's done with classes in Java. However, I am running in sort of an "issue" here. I have many ADT for which I do not need getters nor setters. The only thing I need to do with them is 1. build the structure by extracting data from a byte array and 2. display the extracted data stored in the ADT on the screen of my device. To display the data, I'm using an external statically compiled library, and am accessing some of the drivers of my screen. I now would like to unit test the function that extracts the data from the byte array and builds my ADT. However, I do not have getters not setters to access the members of my structure. So, the only way for me to really unit test the function is by calling the display function, which is not really unit testable if I do not run the unit tests on an emulator and mocking the drivers. Is it "clean" in this case to implement getters and setters if they are only meant to be used in my unit tests?

To give a better example of my problem, assume I have an ADT that represent a TLV (tag length value) buffer:

in tlv.h, I'd have the following:

struct tlv typedef tlv_t;

// Builds a tlv_t struct from the tlv data stored in a byte buffer
tlv_t * extract_tlv(const uint8_t *buffer, size_t buffer_length);

// display the tlv data stored in the tlv structure on my device screen.
int display_tlv(const tlv_t *tlv);

and in the tlv.c, I'd have the following:

#include "tlv.h"

typedef enum
{
  TAG_A,
  TAG_B,
  ...
} tlv_tag_t;

struct tlv {
  tlv_tag_t tag;
  size_t length;
  uint8_t *value;
};


tlv_t * extract_tlv(const uint8_t *buffer, size_t buffer_length) {
   tlv_t *tlv = (tlv_t *)calloc(1, sizeof(tlv_t));
   if(!tlv)
   {
      return NULL;
   }

   // extract the tlv data in buffer and stores them in the tlv struct
   ...

   return tlv;
}


int display_tlv(const tlv_t *tlv) {
   // accesses the field of my tlv struct, and display them
   ...
}

Image I have the following buffer 0x00010004012345678. The tag and the length are uint16_t values in the buffer, so, when calling extract_tlv with the buffer above, I'd expect ending with the following tlv structure:

tlv.tag    = TAG_B,                      // 0x0001
tlv.length = 4,                          // 0x0004
tlv.value  = {0x12, 0x34, 0x56, 0x78},   // 0x12345678

Now, I would like to unit test this extract_tlv function, to be sure that if I send the buffer above, I get the structure above as an output. How can I do that in a clean way if I do not have getters and setters ? I think implementing getters and setters just for your unit test is not a good practice, because they won't make it to the production code, so, they should be used in your unit tests. An other approach we've tried is having the members of the tlv struct in a define, located in the tlv.h file. In our test files, we create a test_tlv struct, that uses the defines to delcare its member, and we do the same for the tlv_t struct in the tlv.c file. Then, we cast every tlv_t struct into a test_tlv_t struct in our unit tests, and just like that, we can access the members without having getters and setters:

In tlv.h:

#defin TLV_STRUCT_MEMBER \
  tlv_tag_t tag; \
  size_t length; \
  uint8_t *value;

typedef enum
{
  TAG_A,
  TAG_B,
  ...
} tlv_tag_t;

struct tlv typedef tlv_t;

// Builds a tlv_t struct from the tlv data stored in a byte buffer
tlv_t * extract_tlv(const uint8_t *buffer, size_t buffer_length);

// display the tlv data stored in the tlv structure on my device screen.
int display_tlv(const tlv_t *tlv);

In tlv.c:

struct tlv {
  TLV_STRUCT_MEMBER 
};


tlv_t * extract_tlv(const uint8_t *buffer, size_t buffer_length) {
   ...
}


int display_tlv(const tlv_t *tlv) {
   ...
}

and in test_tlv.c

#include "tlv.h"

struct test_tlv {
  TLV_STRUCT_MEMBER 
} typedef test_tlv_t;


TEST_EXTRACT_TLV() {
  test_tlv_t *tlv = (test_tlv_t *)extract_tlv(...);
  TEST_ASSERT_EQUAL(8, tlv.length);
  ...
}

But this solution is a bit hacky, and I'm not a big fan of casting my ADT into an other, even though they are technically the same.

What is the best "clean" practice here? Is there a good solution?


Solution

  • Unit tests are actually white box tests, not blackbox. Nobody hinders you to access internals for these tests. Here are 2 more options:

    • In your test_tlv.c code, you include the tlv.c and not compile tlv.c separately

      • Advantage: you get access to internals of the module (but not to function scoped variables)

    test_tlv.c

    #include "tlv.h"
    // include the module itself to access internals for tests
    // don't compile and link tlv.c in the unitt test env
    #include "tlv.c"
    
    void test_extract_1(void) {
        uint8_t test1[] = "0x00010004012345678";
        
        tlv_t* res = extract_tlv(test1, sizeof(test1));
        
        TEST_ASSERT_NOT_EQUAL( res, NULL);
        TEST_ASSERT_EQUAL( res->tag, TAG_B); // <- 'tag' available through include tlv.c
        // ...
    }   
    
    • Separate e.g. the struct tlv type in tlv_privtypes.h, which is normally only included in tlv.c, but for your tests, you ca include the tlv_privtypes.h additionally in your test_tlv.c:

    tlv_privtypes.h

    #ifndef TLV_PRIVTYPES_H_INCLUDED
    #define TLV_PRIVTYPES_H_INCLUDED
    
    typedef enum {
        TAG_A,
        TAG_B,
        ...
    } tlv_tag_t;
     
    struct tlv {
        tlv_tag_t tag;
        size_t length;
        uint8_t *value;
    };
    
    #endif
    

    tlv.h

    #ifndef TLV_H_INCLUDED
    #define TLV_H_INCLUDED
    
    typedef struct tlv tlv_t;
    
    // Builds a tlv_t struct from the tlv data stored in a byte buffer
    tlv_t * extract_tlv(const uint8_t *buffer, size_t buffer_length);
    
    // display the tlv data stored in the tlv structure on my device screen.
    int display_tlv(const tlv_t *tlv);
    
    #endif
    

    tlc.c

    #include "tlv_privtypes.h"
    #include "tlv.h"
    
    tlv_t * extract_tlv(const uint8_t *buffer, size_t buffer_length) {
        //  ...
    }
    
    int display_tlv(const tlv_t *tlv) {
        //  ...
    }
    

    test_tlv.c

    #include "tlv.h"
    #include "tlv_privtypes.h" // access to internal types for tests
    
    void test_extract_1(void) {
        uint8_t test1[] = "0x00010004012345678";
        
        tlv_t* res = extract_tlv(test1, sizeof(test1));
        
        TEST_ASSERT_NOT_EQUAL( res, NULL);
        TEST_ASSERT_EQUAL( res->tag, TAG_B); // <- 'tag' available through include tlv_privtypes.h
        // ...
    }
    

    main.c - normal user file

    #include "tlv.h" // normal users just include tlv.h
    
    int main(void) {
        uint8_t buffer[MAX_BUFLEN] = {0};
        
        int rxlen = UART_Receive(buffer, MAX_BUFLEN);
        
        tlv_t* res = extract_tlv(buffer, rxlen);
        display_tlv(res);
        
        return 0;
    }