【技术专辑】为什么要使用单元测试来编写更好的嵌入式软件

单元测试可以帮助您编写更好的嵌入式软件。这是如何做。

 

单元测试是您编写的其他软件功能,用于测试应用程序的软件“单元”。这些测试可帮助您确保嵌入式软件正常运行 - 现在和随着时间的推移而变化。

 

在嵌入式C应用程序中,“单元”通常是单个源文件(和相应的头文件)。该源“模块”通常是系统某些部分的抽象,并实现一组相关功能,例如环形缓冲区或协议解析器。

 

该模块的单元测试是一组执行“被测模块”的功能。单元测试函数以特定顺序和特定参数调用被测模块的功能 - 并验证模块是否返回正确的结果。

 

这些测试功能不包含在您的发布版本中,但是在开发期间(或在任何更改之后)运行以测试您的应用程序。

 

通常,每个要测试的源模块都有一个相应的单元测试文件,其中该模块的所有单元测试都会进行。

 

【技术专辑】为什么要使用单元测试来编写更好的嵌入式软件

 

例如,如果您有自己的某些数据结构实现 - 比如堆栈 - 那么单元测试可能会调用push和pop函数来确保堆栈在各种条件下的行为与预期一致。

 

以下是其中一个功能的示例:

 

                    #include "some_test_framework.h"

#include "my_stack.h"

 

// A test for my_stack.

void test_WhenIPushAnItem_ThenTheCountIncreases(void)

    // Do something.

    stack_push('a');

    

    // Make sure it had the right effect.

    ASSERT(stack_get_count() == 1);

}

                  

这个特殊的测试只是冰山一角。我们可以快速轻松地为其他条件添加更多测试 - 甚至是在您的软件运行时不太可能遇到的情况。

 

例如,堆栈在填满时如何表现?当然,我想  我知道发生的基础上如何我写的这是怎么回事,但如果将这些代码永远居然  叫?

 

提示:我真的希望它不是10年后,当设备在海底一英里时,你无处可寻!

 

如果为此案例创建单元测试,则可以立即运行该代码并确保它实际执行的操作。

 

                    // Another test for my_stack.

void test_GivenTheStackIsFull_WhenIPushAnotherItem_ThenItIsRejected(void)

{

    // Fill the stack.

    for (int i = 0; i < 100; i++)

    {

        stack_push('a');

    }

 

    // Try to push another.

    bool success = stack_push('a');

    

    // Make sure it was rejected.

    ASSERT(success == false);

    ASSERT(stack_get_count() == 100);

}

                  

这对嵌入式软件尤其重要,因为它必须处理真实硬件。对于硬件,您通常无法行使其所有行为,因此很难确切知道您的软件将处理所有这些问题。

 

例如,当我的温度传感器读取舒适的72度 - 我办公室的温度时,如何在所有温度范围内测试我的温度转换逻辑?

 

我想  我可以将我的硬件放在冰箱或热室中,但这将是1)需要一些体力来设置和2)不是非常可重复的。

 

正如您现在可能已经猜到的那样,更好的选择是将所有温度转换逻辑放在自己的源模块中,并为其编写一系列单元测试。我可以输入我想要的任何原始传感器值(包括错误)并检查每个传感器值是否正确处理。

 

单元测试的目标是独立于系统的其他部分测试您的软件“单元”。您将单位视为黑匣子,按特定顺序调用函数并使用特定参数,并验证您是否获得了正确的结果。单独测试的原因是,当出现问题时,您确切地知道问题所在 - 在被测模块中。

 

大多数源模块都有依赖关系。要单独测试模块,您不能包含它可能依赖的其他模块。那么你需要做什么?啊,答案是你需要“模仿”那些依赖关系。

 

模拟是模块的虚假实现,允许您模拟和检查与之交互。您可以控制模拟的行为方式,以便您可以完全锻炼被测模块。 

 

在温度传感器示例中,温度传感器驱动器(带有转换逻辑)可能需要使用I2C驱动器与传感器通信。要单独测试临时传感器驱动程序,您需要模拟I2C驱动程序。 

 

【技术专辑】为什么要使用单元测试来编写更好的嵌入式软件

 

I2C驱动程序模拟允许您在调用I2C驱动程序时将所需的测试数据返回给临时传感器驱动程序。当读取当前温度寄存器时,而不是实际输出硬件,而只是告诉它返回0xFF(或任何你想要的值)。

 

模拟I2C驱动程序的另一个好处是它可以消除 测试中的任何硬件依赖性。这意味着您实际上并不需要真正的硬件来测试应用程序。您可以编译测试并在主机PC上运行它们。

 

听起来很棒,对吧?好。那么,你如何真正做到这一点?好的,我正在接受这个。

 

任何单元测试设置都有两个主要组件:单元测试框架本身和模拟框架。单元测试框架允许您定义和执行测试,并为您提供一些“断言”函数来断言特定测试已通过或失败。模拟框架是您用来模拟依赖项并测试每个模块隔离的方法。

 

如果您在Visual Studio中开发.NET应用程序或在Eclipse中开发Java应用程序,则单元测试支持内置于IDE中。您只需设置测试并单击“运行测试”按钮。这是自动测试发现,非常方便。正确设置测试文件后,测试框架可以在一个步骤中自动运行所有测试。

 

如果您在C中编写嵌入式应用程序,那么现在最好的选择是Ceedling。它是一个围绕Rake构建的单元测试系统(就像make而不是Ruby语言)。要使用它,您需要安装Ruby,但实际上您不必了解Ruby。

 

Ceedling使用Unity作为其单元测试框架,使用CMock作为其模拟框架。它之所以如此出色,是因为它提供了自动测试发现和执行。这使得快速启动和运行变得容易。如果你正确地问它,它也会自动生成模拟模块。

 

Ceedling旨在通过在主机PC上运行测试来工作 - 而不是在目标硬件上运行。使用本机编译器(默认为gcc)编译测试。这意味着测试可以快速运行 - 无需等待闪存硬件 - 并且可以在开发过程中连续运行而不会降低速度。

 

由于测试在主机PC上运行,因此需要模拟所有硬件依赖性 - 就像上面温度传感器中的I2C驱动程序一样。由于测试在PC上运行,因此测试无法访问目标处理器的I2C寄存器,因为它们不存在。

 

这鼓励了一个设计良好的分层架构,其中硬件接口与其他应用程序逻辑分离。

 

你有没有做过硬件尚未准备好的项目?还是没有足够的去处?或者它正在改变下一局的转速?能够在没有硬件的情况下开发和测试一些或甚至大多数应用程序可以在每种情况下提供帮助。 

 

你仍然需要在某些时候在真实硬件上进行测试,但如果没有它,你可以走得很远。

 

由于应用程序是由一堆单独的单元测试模块构建的,因此当您在真实硬件上进行测试时,测试将会少得多。您只是测试 这些模块的集成。并且...最好的部分是找到和修复的错误会更少。

  • 【技术专辑】为什么要使用单元测试来编写更好的嵌入式软件已关闭评论
    A+
发布日期:2019年03月04日  所属分类:参考设计