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

98 views Asked by At

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?

2

There are 2 answers

2
kesselhaus On BEST ANSWER

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;
}
5
the busybee On

(After clarification...) Since all members of your ADT are private, you will not test their values. It is the same case with the standard type FILE, of which we only use pointers to. Like you do with your ADT.

Instead, test what is specified in your "contract": The extract function scans the byte array and fills the structure. The display function shows the values of the structure. The only publicly visible data flow is from the byte array to the display output, the intermediate representation is opaque.

You did not say so, but there might be some error detection in the functions. Test these, too. You will need to mock the error reaction function(s).

Concerning the display function, yes, that means you need to mock the display driver.

OT: If you declare the members of a structure private, do not publish them in the header file...