1

我正在对嵌入式 C 代码进行单元测试,而无需在目标硬件上运行。这是代码的一部分:

uint8 tempReadback = 0;

write_to_i2c( msg->addres, msg->value );


     tempReadback = read_from_i2c( msg->addres);

     if( tempReadback == msg->value )
     {
        somethingA;
     }

     else
     {
        somethingB;
     } 

函数write_to_i2c()将值写入特定寄存器。函数read_from_i2c()从寄存器读回值。此外,我使用变量tempReadback来比较读回的值是否与写入的值相同。到目前为止还可以,这适用于目标硬件。现在我正在做 Uni Tests 而不在目标硬件(循环中的软件)上运行代码。这意味着,表达式tempReadback == msg->value永远不会为真(tempReadback 为 0),我每次都会在语句somethingB中运行。有没有办法伪造寄存器回读?我使用 CppUTest 作为框架。

将不胜感激!

4

2 回答 2

3

CppUTest非常适合嵌入式 C 开发,因为它是唯一允许模拟自由函数(在您的情况下,write_to_i2c()read_from_i2c())的测试框架。

现在,您应该真正阅读 CppUTest 文档或优秀的书Test Driven Development for Embedded C

无论如何,下面的代码显示了如何做到这一点。

您的被测单元 (UUT),以可编译的方式编写(下次您提出关于 SO 的问题时,请努力):

#include "temperature.h"
#include "i2c.h"

void somethingA(void) { }
void somethingB(void) { }

void temperature_do(uint8_t address, uint8_t value) {
    write_to_i2c(address, value);
    const uint8_t tempReadback = read_from_i2c(address);
    if (tempReadback == value) {
        somethingA();
    } else {
        somethingB();
    }
}

正如您所写,我们需要“伪造”,或者更确切地说,我们需要“模拟”write_to_i2c()read_from_i2c(). 我们将模拟放在一个单独的文件中,比如 i2c_mock.cpp,以便在构建单元测试时,我们链接模拟而不是实际实现:

extern "C" {
#include "i2c.h"
};

#include "CppUTestExt/MockSupport.h"

void write_to_i2c(uint8_t address, uint8_t value) {
    mock().actualCall(__FUNCTION__)
        .withParameter("address", address)
        .withParameter("value", value);
}

uint8_t read_from_i2c(uint8_t address) {
    mock().actualCall(__FUNCTION__)
        .withParameter("address", address);
    uint8_t ret = mock().returnIntValueOrDefault(0);
    return ret;
}

有关详细信息,请参阅 CppUMock 文档。这只是经典的 CppUMock 样板。

最后一部分是单元测试:

extern "C" {
#include "temperature.h" // UUT
};

#include "CppUTest/TestHarness.h"
#include "CppUTest/CommandLineTestRunner.h"
#include "CppUTestExt/MockSupport.h"

TEST_GROUP(Temperature)
{
    void setup() {}
    void teardown() {
        mock().checkExpectations();
        mock().clear();
    }
};

TEST(Temperature, somethingA)
{
    const uint8_t value = 10;
    mock().ignoreOtherCalls();
    mock().expectOneCall("read_from_i2c").ignoreOtherParameters()
        .andReturnValue(value);
    temperature_do(10, value);
}

TEST(Temperature, somethingB)
{
    const uint8_t value = 10;
    mock().ignoreOtherCalls();
    mock().expectOneCall("read_from_i2c").ignoreOtherParameters()
        .andReturnValue(value+1);
    temperature_do(10, value);
}

int main(int argc, char** argv) {
    return CommandLineTestRunner::RunAllTests(argc, argv);
}

这个 UT 实际上会提供 100% 的分支覆盖率。同样,我无法解释所有细节。如果您观察和比较测试用例somethingAsomethingB您将看到需要什么才能使 UUT 一次进入调用路径,somethingA()一次进入调用路径somethingB()

让我们举个例子

mock().expectOneCall("read_from_i2c")
    .ignoreOtherParameters()
    .andReturnValue(value+1);

在这里,我们对 CppUmock 说期望调用 function read_from_i2c(),忽略参数是什么,并且,这是最重要的,返回value + 1(或任何你喜欢的与 不同的东西value)。这将导致 UUT 进入调用somethingB().

快乐的嵌入式 C 开发和快乐的单元测试!

于 2016-04-21T20:54:34.350 回答
0

我看一下您提出的关于 TDD 和模拟对象的书。好的,据我了解,例如在这一行中(已经创建了模拟):

mock().expectOneCall("read_from_i2c").ignoreOtherParameters()
    .andReturnValue(value);
temperature_do(10, value);

程序“跳转”到模拟的“read_from_i2c”函数(来自 i2c_mock.cpp),参数由我自己定义,而不是来自被测单元的真实参数?或者我们真的从被测单元中调用了我们的函数,但是我们使用模拟中定义的参数来操作这个函数?

于 2016-04-29T09:28:11.843 回答