C语言 如何在没有getter和setter的情况下对抽象数据类型进行单元测试?

k7fdbhmy  于 2023-08-03  发布在  其他
关注(0)|答案(2)|浏览(73)

我目前正在尝试重构我的一些嵌入式C代码,并试图应用詹姆斯W。Grenning在他的Test-Driven Development for Embedded C书中推荐。为此,我将代码划分为每个抽象数据类型的模块,有点像Java中的类。然而,我在这里遇到了一个“问题”。我有很多ADT,我不需要getter和setter。我唯一需要做的就是1。通过从字节数组和2中提取数据来构建结构。在我的设备屏幕上显示ADT中存储的提取数据。为了显示数据,我使用了一个外部静态编译库,并访问了我的屏幕的一些驱动程序。现在,我想对从字节数组中提取数据并构建ADT的函数进行单元测试。但是,我没有getter和setter来访问我的结构的成员。因此,真正对函数进行单元测试的唯一方法是调用display函数,如果不在模拟器上运行单元测试并模拟驱动程序,则display函数实际上是不可测试的。在这种情况下,如果getter和setter只打算用在我的单元测试中,那么实现它们是否是“干净的”?
为了更好地给予我的问题,假设我有一个表示TLV(标记长度值)缓冲区的ADT:
tlv.h中,我会得到以下结果:

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);

字符串
tlv.c中,我会有以下内容:

#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
   ...
}


图I具有以下缓冲区0x00010004012345678。标记和长度是缓冲区中的uint16_t值,因此,当使用上述缓冲区调用extract_tlv时,我希望以下面的TLV结构结束:

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


现在,我想对这个extract_tlv函数进行单元测试,以确保如果我发送上面的缓冲区,我会得到上面的结构作为输出。如果我没有getter和setter,我怎么能以一种干净的方式做到这一点?我认为只为单元测试实现getter和setter不是一个好的做法,因为它们不会应用到生产代码中,所以,它们应该用在单元测试中。我们尝试过的另一种方法是将tlv结构体的成员放在一个define中,位于tlv.h文件中。在我们的测试文件中,我们创建了一个test_tlv结构,它使用defines来删除它的成员,我们对tlv.c文件中的tlv_t结构执行了相同的操作。然后,在单元测试中,我们将每个tlv_t结构转换为一个test_tlv_t结构,就像这样,我们可以在没有getter和setter的情况下访问成员:
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);


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) {
   ...
}


以及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);
  ...
}


但是这个解决方案有点笨拙,我不太喜欢把我的ADT转换成另一个,尽管它们在技术上是一样的。
这里最好的“干净”做法是什么?是否有好的解决方案?

monwx1rj

monwx1rj1#

  • (澄清后...)* 由于ADT的所有成员都是私有的,因此您不会测试他们的值。这与标准类型FILE的情况相同,我们只使用指针。就像你和你的ADT一样。

相反,测试你的“contract"中指定的内容:extract函数扫描字节数组并填充结构。display函数显示结构的值。唯一公开可见的数据流是从字节数组到显示输出,中间表示是不透明的。
你没有这么说,但在函数中可能有一些错误检测。把这些也测试一下。您需要模拟错误React函数。
关于显示功能,是的,这意味着您需要模拟显示驱动程序。
OT:如果你声明一个结构的成员是私有的,不要在头文件中发布它们…

wrrgggsh

wrrgggsh2#

单元测试实际上是白色盒测试,而不是黑盒测试。没有人会阻止您访问这些测试的内部。这里还有两个选项:

  • 在test_tlv.c代码中,包含tlv.c,而不单独编译tlv.c
  • 优点:可以访问模块的内部(但不能访问函数作用域的变量)

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
    // ...
}

字符串

  • 分开,例如tlv_privtypes.h中的struct tlv类型,通常只包含在tlv.c中,但对于您的测试,您可以在test_tlv.c中额外包含tlv_privtypes.h

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 -普通用户文件

#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;
}

相关问题