开源改变世界

单元测试 #211

推推 grbl 3年前 (2022-10-27) 377次浏览 0个评论
打开
Protoneer 打开了这个问题 on 22 Mar 2013 · 17 条评论
打开

单元测试#211

Protoneer 打开了这个问题 on 22 Mar 2013 · 17 条评论

注释

单元测试 #211
贡献者

Protoneer 评论 on 22 Mar 2013

前几天我们讨论了下一步该去哪里,其中一个主题是单元测试。

单元测试是我日常开发中不可或缺的一部分,但作为一名全职嵌入式开发人员,我正试图将其融入我当前和未来的项目中。

关于我们如何实现这一点的任何建议?

单元测试 #211
贡献者作者

Protoneer 评论 on 22 Mar 2013

以下是我的一些想法:

然后可以通过让 PC 应用程序捕获结果并将其与显示失败/通过的“答案”表进行比较来自动化上述过程。

单元测试 #211
贡献者作者

Protoneer 评论 on 22 Mar 2013

有类似 http://code.google.com/p/arduinounit/的东西,但它们占用了空间,而且 GRBL 已经填满了这个小 MCU。:)

我们总是可以采用无硬件的方式,只对库进行单元测试。可以完全自动化,但我们与实际结果有多接近?

好吧,它开放讨论……

单元测试 #211

是否值得考虑模拟。取决于所需
的测试级别。硬件方面使单元测试更加
有趣。

或者也许应该使代码的某些方面(不是全部)
足够通用以在计算机上运行。然后在微控制器或模拟中对微控制器特定的东西进行
单元测试。

我希望我说得有道理,目前同时走路、打字和思考
=p
在 22/03/2013 12:57 PM,“Bertus Kruger” notifications@github.com写道:

有类似http://code.google.com/p/arduinounit/的东西,但它们占用
了空间,而且 GRBL 已经填满了这个小 MCU。:)

我们总是可以采用无硬件的方式,只对库进行单元测试。
可以完全自动化,但我们与实际结果有多接近?

好吧,它开放讨论……


直接回复此邮件或在 GitHub 上查看 https://github.com/ /issues/211 #issuecomment-15278668

单元测试 #211

“在 PC 上编译”方法是有意义的,但在大多数情况下是无用的,原因有多种,例如 avr-gcc 中存在 GCC 错误,但在 gcc 和硬件行为细节中缺少/未触发。

simulavr 方法很有意义,特别是如果测试设计为自包含的(即它们仅依赖于交付的一些 G 代码,并且 GPIO 会因此以非常明确的方式发生变化)。最新的 simulavr 知道 ATmega328p,所以我们应该没问题。以前版本的 Atmel Studio (5.x IIRC) 还带有一个功能齐全且可编写脚本的模拟器,因此也可以通过这种方式取得一些进展。

单元测试 #211

我不认为我可以为这次谈话做出太多贡献,因为这对我来说是一个全新的领域。我不是专业的程序员,所以我不知道你们都用来证明代码的一些更高级的技术。我处理的大部分东西都小到一个人可以掌握和维护。Grbl 可以说是足够小了,至少目前是这样。

我认为这是我们选择保留非常有限且很小的主要原因之一。我们可以在一个小的代码库中解决一些最困难的问题,这最终让我们变得灵活。我认为随着我们在 ARM 上的规模越来越大,开始自动化一些测试任务是个好主意,但我不知道最好的方法是什么。

就我自己而言,我在编写代码时真的没有做任何特别的事情。从某些标准来看,它可能有点愚蠢和陈旧。我只是编译代码并将其闪存到 Arduino 上,然后验证新代码是否按预期工作。通过翻转 I/O 引脚并在示波器上测量时间来完成时序和代码效率测量。除此之外,我有意识地尝试将功能分开,以便新代码不会影响其他进程。如果它们确实必须交织在一起,我会将其保持在绝对最低限度并尽可能简单。作为最终测试,我在几台机器上使用了一个极端案例“编织”g 代码程序(很多曲线)进行最终校对。

这肯定会很耗时,所以我很高兴听到我们有什么样的选择。

单元测试 #211

我这样做的方法介于 DIY 黑客所做的和“真正的”软件工程师所做的之间:
我首先为主机编写代码(即用 gcc 编译),只是为了感受一下它的方式会看起来和行为。我选择了一个 API,并且随着开发的进行,我会分解出现的任何模块。通过这样做,很容易 (1) 脚本测试和 (2) 将单向的代码与完全不同的代码(例如 G 代码解析器与步进代码)隔离开来。
根据问题的复杂程度,我尝试将尽可能多的独立于平台的代码(即仅使用 ANSI C 中的代码)集中在一起,我称之为“可移植”的一半。然后,我列出了跨越便携式和非便携式区域之间边界的所有点,并将该列表转换为 HAL API。留在非便携式区域中的所有内容都是特定于硬件的,并且需要针对每个新架构进行更改。
使用经典测试框架对便携式一半进行测试,而对于非便携式一半,同样取决于它的重要性,存在各种技巧,包括反复刷新芯片和使用逻辑分析仪查看 GPIO 引脚或运行 simulavr .

我发现在嵌入式世界中,大多数“调试”实际上是“从一开始就编写正确的代码”(比如@chamnit提到),因为在嵌入式芯片中进行类似主机的调试的空间并不大。当然,如果您碰巧继承了一笔财富,您总是可以购买“官方”工具(debugWire 等)——但这超出了当前讨论的范围。

ARM 是另一回事(就像 XMegas 一样),因为它具有成熟的 JTAG 接口,并且实际上可以像计算机中的 CPU 一样进行调试:步进、检查变量、添加条件断点等。工作正常。此外,大多数 ARM 都有足够大的 RAM(或者,对于那些不能使用外部 RAM 芯片的主板有),您可以将代码上传到 RAM 中,从而使事情变得更快(更不用说让 Flash 持续更长时间了)。此外,由于它是 32 位架构,gcc 的行为与主机的 gcc 非常相似,因此任何存在的编译器错误都更有可能在主机和 ARM gcc 上都出现,因此在主持人,然后认为这些结果值得。

单元测试 #211

是的,我喜欢在哪里@csdexter正在这样做。这几乎就是
我脑海中漂浮的东西。

grbl 中自动测试的另一大挑战是有很多
全局变量。并不是说应该更改,因为它有助于
压缩代码库等。但是全局变量意味着可以
在后台更改的状态。因此,例如,可以运行
两次相同的测试并获得不同的结果。这使自动
测试变得复杂……
在 23/03/2013 上午 1:46,“Radu – Eosif Mihailescu” notifications@github.com
写道:

我这样做的方法介于 DIY 黑客
所做的和“真正的”软件工程师所做的之间:
我首先为主机编写代码(即用 gcc 编译),只是为了
感受一下它的方式会看起来和行为。我选择了一个 API,并且
随着开发的进行,我会分解出现的任何模块。通过这样做,
很容易 (1) 脚本测试和 (2) 将单向
的代码与完全不同的代码(例如 G 代码解析器与
步进代码)隔离开来。
根据问题的复杂程度,我尝试将尽可能
多的平台独立(即仅使用 ANSI 中的东西
C)尽我所能编写代码,我称之为“便携式”一半。然后,我列出了跨越
便携式和非便携式
区域之间边界的所有点,并将该列表转换为 HAL API。留
在非便携式区域中的所有内容都是特定于硬件的,并且需要
针对每个新架构进行更改。
使用经典测试
框架对便携式一半进行测试,而对于非便携式一半,同样取决于
它的重要性,存在各种技巧,包括反复刷新
芯片和使用逻辑分析仪查看 GPIO 引脚或运行
simulavr .

我发现在嵌入式世界中,大多数“调试”实际上是
“从一开始就编写正确的代码”(如@chamnithttps://github.com/chamnit提到的),因为类似主机的调试空间并不大在
嵌入式芯片中。当然,如果您碰巧继承了一笔财富,您
总是可以购买“官方”工具(debugWire 等)——但这超出
了当前讨论的范围。

ARM 是另一回事(就像 XMegas 一样),因为它具有
成熟的 JTAG 接口,并且实际上可以像
计算机中的 CPU 一样进行调试:步进、检查变量、添加条件断点
等。工作正常。此外,大多数 ARM 都有足够大的 RAM(或者,对于那些不能
使用外部 RAM 芯片的主板有),您可以将代码上传到
RAM 中,从而使事情变得更快(更不用说让 Flash 持续
更长时间了)。此外,由于它是 32 位架构,gcc 的行为
与主机的 gcc 非常相似,因此任何
存在的编译器错误都更有可能在主机和 ARM gcc 上都出现,因此
在主机,然后考虑这些结果
值得。


直接回复此邮件或在 GitHub 上查看 https://github.com/ /issues/211 #issuecomment-15304200

单元测试 #211
贡献者作者

Protoneer 评论 2013 年 3 月 23 日

@kitizz您所描述的可以通过依赖注入来解决。使用依赖注入,您可以通过参数传入函数所需的所有内容。这有助于单元测试过程,因为单元测试现在可以一致地测试与将传递给应用程序内部函数的输入相同的输入。

DI 和 UT 齐头并进。DI 保持测试输入的一致性,UT 确保我们坚持使用 DI,因为 UT 将无法访问全局变量。

单元测试 #211

是的,我想我想知道拥有全局变量可以节省多少内存。如果我有空闲时间,我很乐意重构 grbl
以使用 DI。看看它如何影响大小。
2013 年 3 月 23 日下午 5:48,“Bertus Kruger” notifications@github.com写道:

@kitizz https://github.com/kitizz你所描述的可以
通过依赖注入来解决。使用依赖注入,您可以
通过参数传入函数所需的所有内容。这有助于单元测试过程
,因为单元测试现在可以一致地测试与将
传递给应用程序内部函数的输入相同的输入。

DI 和 UT 齐头并进。DI 保持测试输入的一致性,
UT 确保我们坚持使用 DI,因为 UT 将无法访问全局
变量。


直接回复此邮件或在 GitHub 上查看 https://github.com/ /issues/211 #issuecomment-15333326

单元测试 #211

我在嵌入式系统中用于管理全局变量的一种技术是将变量组隔离为单例。单例是只能有一个实例的结构(或对象)。因此,与其拥有一堆全局变量,不如将它们收集在一个整洁的包中。有些人认为使用单例是不好的巫术(他们称其为“反模式”,我更喜欢前一个术语),但话说回来,全局变量也可以这样说。至少对于单身人士,您可以将它们全部收集在整齐的捆绑中。很多 grbl 实际上是以这种方式工作的——就像配置值一样,但这些结构实际上并不称为单例(如果我弄错了,请纠正我)。

单例的使用比依赖注入少了一步——它们实际上并没有传递给函数——它们只是假设在那里。由于堆栈帧会更大,因此在运行时传递给函数会稍微降低 CPU 效率。还有其他可能的运行时效率低下 – 例如间接寻址与直接寻址。但这些对于大多数功能可能无关紧要(分析的情况)。我不确定它是否会对 RAM 使用产生负面影响——我怀疑不会。

因此,拥有关于在函数中使用哪些单例的标题文档将允许某种范围管理知道在给定的单元测试期间需要担心哪些变量。当然,展开调用序列是必要的,而且会变得很麻烦。

我发现另一件有用的事情是将单元测试代码放在每个模块的末尾,只有在定义了 __UNIT_TEST_XXXXX 时(通常在 module.h 头文件中),它才会被 #ifdef’d 放入。在 inits 之后但系统主循环之前的 main 中有一个 _unit_test() 函数。通过有选择地编译测试,您可以在测试期间管理 FLASH 占用空间,并且它不会向运行时占用空间添加任何内容。

最后,我通过在初始化期间预加载串行缓冲区来伪造初始串行输入。代码开始然后开始执行指定的功能。这对于设置模拟很有用。AVRStudio4 为 mega 和 xmega 部件提供了一个非常好的模拟环境。AtmelStudio6 也可以,但有一些尚未克服的限制(例如结构中字符串的良好 ASCII 显示,以及其他一些)。

您可以查看 TInyG 代码库以获取示例。测试有些地方有点乱,但我合理化了它不是运行时代码的一部分,所以我可能会马虎。

奥尔登

单元测试 #211

不,单例对于单元测试作为全局变量同样不利。您
会遇到相同的问题:可能两次运行相同的功能并获得不同的
结果。我不称它为反模式,因为我同意这是
存储全局变量的好方法。问题在于全局变量位

一些文献:http:
//blogs.msdn.com/b/scottdensmore/archive/2004/05/25/140827.aspx

http://tech.puredanger.com/2007/07/03/pattern-hate-singleton/

http://googletesting.blogspot.com.au/2008/11/clean-code-talks-global-state-and.html
2013 年 3 月 23 日晚上 8:20,“Alden Hart” notifications@github.com写道:

我在嵌入式系统中用于管理全局变量的一种技术是
将变量组隔离为单例。
单例是只能有一个实例的结构(或对象)。因此,与其拥有一堆
全局变量,不如将它们收集在一个整洁的包中。有些人
认为使用单例是不好的巫术(他们称其为
“反模式”,我更喜欢前一个术语),但话说回来,
全局变量也可以这样说。至少对于单身人士,您可以将它们全部
收集在整齐的捆绑中。很多 grbl 实际上是以这种方式工作的 – 就像
配置值一样,但这些结构实际上并不称为单例(
如果我弄错了,请纠正我)。

单例的使用比依赖注入少了一步——它们
实际上并没有传递给函数——它们只是假设在
那里。传递给函数在运行时
的 CPU 效率会降低,因为堆栈帧会更大。我不确定它是否
会对 RAM 使用产生负面影响——我怀疑不会。

因此,拥有关于在函数中使用哪些单例的标题文档
将允许某种范围管理知道
在给定的单元测试期间需要担心哪些变量。当然,展开调用
序列是必要的,而且会变得很麻烦。

我发现另一件有用的事情是将单元测试代码放在每个
模块的末尾,只有在定义了 __UNIT_TEST_XXXXX 时
(通常在 module.h 头文件中),它才会被 #ifdef’d 放入。
在 inits之后但系统主循环之前的 main中有一个 _unit_test() 函数。通过有选择地编译
测试,您可以在测试期间管理 FLASH 占用空间,并且它
不会向运行时占用空间添加任何内容。

最后,我通过在初始化期间预加载串行缓冲区来伪造初始串行输入
。代码开始然后开始执行指定的功能。
这对于设置模拟很有用。
AVRStudio4为 mega 和 xmega 部件提供了一个非常好的模拟环境。AtmelStudio6 也可以,
但有一些尚未克服的限制(例如
结构中字符串的良好 ASCII 显示,以及其他一些)。

您可以查看 TInyG 代码库以获取示例。测试有些地方有点
乱,但我合理化了它不是运行时
代码的一部分,所以我可能会马虎。

奥尔登


直接回复此邮件或在 GitHub 上查看 https://github.com/ /issues/211 #issuecomment-15334894

单元测试 #211

我看到了作者提出的观点,但让我推后一点。另外,我并不是说单例是编码的“正确方法”,我只是说如果您使用全局变量,它们是管理它们的更好方法。

此外,作者正在处理一些字节和周期来处理 DI 的环境是无关紧要的——我们不是。或者至少我们可能不需要 – 需要分析。权衡是,在紧凑的嵌入式环境中,使用 DI “免费”提供的东西——依赖跟踪、隔离、解耦……以非零成本出现。但是,如果您使用全局/单例,则必须手动完成 DI 的好处,并且变得更加劳动密集且容易出错。我只是质疑权衡。在任何一种情况下,您仍然需要管理测试的初始条件 – 在 DI 情况下,它们是为您说明的,在另一种情况下,您必须非常仔细地跟踪它们。

单元测试 #211

是的,就性能而言,这就是我对 DI 的疑惑。
但是,我认为从长远
来看,这些好处并不需要密集的劳动。我认为相反,他们通过 – 正如你所说 –
使自动测试变得明显并且更容易处理和调试来减少劳动力。
2013 年 3 月 23 日晚上 8:47,“Alden Hart” notifications@github.com写道:

我看到了作者提出的观点,但让我推后一点。另外,我
并不是说单例是编码的“正确方法”,我只是说如果
您使用全局变量,它们是管理它们的更好方法。

此外,作者正在处理一些字节和
周期来处理 DI 的环境是无关紧要的——我们不是。或者
至少我们可能不需要 – 需要分析。权衡是,在紧凑的
嵌入式环境中,使用 DI “免费”提供的东西——依赖
跟踪、隔离、解耦……以非零成本出现。但是,如果您使用全局/单例,则
必须手动完成 DI 的好处,并且变得更加劳动密集且
容易出错。我只是质疑
权衡。在任何一种情况下,您仍然需要管理
测试的初始条件 – 在 DI 情况下,它们是为您说明的,在另一种情况下
,您必须非常仔细地跟踪它们。


直接回复此邮件或在 GitHub 上查看 https://github.com/ /issues/211 #issuecomment-15335146

单元测试 #211

我想这可能是一个混合体。两种风格都有崩溃案例。单例崩溃是依赖关系图变得如此复杂,以至于您无法合理地管理它,并且您永远无法确定您的测试条件是什么。DI 崩溃是你太重了,以至于每个函数调用在周期和堆栈空间上都变得太昂贵 – 特别是当你嵌套时。在 JVM 中,这无关紧要,但是当堆栈应该保持在 256 字节以下(选择一个数字)时,它可以。

我怀疑正确的答案是两者的仔细混合。很高兴知道规则,这样您就可以知道何时打破它们。例如,我们真的想将设置结构体传递给从它读取的每个函数吗?

单元测试 #211

两个要点:

无论如何,单例都是一个有争议的问题,因为 grbl 不是面向对象的代码
库。

我分享您对性能和 DI 的担忧。这就是为什么我想
分叉一份 grbl 并测试 DI 的重构是如何工作的。此外,我
担心将 DI 与全局变量混合可能会使代码混乱。
对于目前读起来非常棒的源代码来说,这将是一种耻辱
=p

2013 年 3 月 23 日星期六晚上 10:01,Alden Hart通知@github.com 写道:

我想这可能是一个混合体。两种风格都有崩溃案例。单
例崩溃是依赖关系图变得如此复杂,以至于您无法
合理地管理它,并且您永远无法确定您的测试条件是什么。
DI 崩溃是你太重了,以至于每个函数调用
在周期和堆栈空间上都变得太昂贵 – 特别是当你嵌套时。在 JVM
中,这无关紧要,但是当堆栈应该保持在 256
字节以下(选择一个数字)时,它可以。

我怀疑正确的答案是两者的仔细混合。很高兴知道
规则,这样您就可以知道何时打破它们。例如,我们真的想
将设置结构体传递给从它读取的每个函数吗?


直接回复此邮件或在 GitHub 上查看 https://github.com/ /issues/211 #issuecomment-15335982

单元测试 #211

我认为研究使用 Atmel Studio 模拟器和刺激文件可能会有一些好处?您可以在模拟器中获得处理器、IO 和内存视图。Stimuli 文件提供输入和操作,然后将其记录下来。一般来说,Atmel Studio 总体上提供了一个更好的开发环境。它本质上是带有一些限制的 Visual Studio 2010。至少它是一个出色的调试环境,具有完整的执行和数据断点。而且它是免费的 :-) 支持大多数 Atmel 芯片、AVR 和 ARM

单元测试 #211

我刚刚尝试在 Atmel Studio 中使用模拟器,如果我们伪造 serial_read()(即仅模拟器子例程)将数据馈送到 protocol.c 的第 108 行,那么您可以将代码块/行输入到仅模拟器串行-行变量,并像从串行端口进来一样处理它。在 IO 视图中,您可以看到各种端口位的上升和下降。

喜欢 (0)

您必须 登录 才能发表评论!