开源改变世界

单元测试 #211

推推 grbl 3年前 (2023-01-21) 220次浏览

打开
Protoneer 打开了这个问题 2013 年 3 月 22 日 · 17条评论
打开

单元测试#211

Protoneer 打开了这个问题 2013 年 3 月 22 日 · 17条评论

注释

单元测试 #211
贡献者

前几天,我们讨论了下一步的方向,其中一个主题是单元测试。

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

关于我们如何实施这个的任何建议?

单元测试 #211
贡献者作者

以下是我的一些想法:

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

单元测试 #211
贡献者作者

有像 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 中,从而使速度更快(更不用说使闪存持续时间更长)。此外,由于它是一个 32 位架构,gcc 的行为与主机的 gcc 非常相似,因此无论存在什么编译器错误,主机和 ARM gcc 上都有可能出现,因此在主持人,然后考虑这些结果是值得的。

单元测试 #211

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

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

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

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

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


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

单元测试 #211
贡献者作者

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

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

单元测试 #211

是的,我想我想知道通过使用全局变量可以节省多少内存。如果我有空闲时间,我很乐意重构 grbl
以使用 DI。查看它如何影响大小。
在 23/03/2013 下午 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。在 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
23/03/2013 晚上 8:20,“Alden Hart” notifications@github.com写道:

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

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

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

我发现另一件有用的事情是将单元测试代码放在每个
模块的末尾,只有在定义了 __UNIT_TEST_XXXXX 时
(通常在 module.h 标头中)才使用#ifdef。
在 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 的疑惑。
但是,从长远来看,我并不认为这些好处需要高强度的劳动
。我会反驳,正如你所说,它们通过
使自动测试变得明显并且更易于处理和调试来减少劳动力。
在 23/03/2013 下午 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。这就是为什么我想
Fork 一个 grbl 的副本并测试 DI 的重构是如何工作的。此外,我
担心将 DI 与全局变量混合可能只会使代码混乱。
对于目前阅读起来如此美妙的源代码来说,这将是一种耻辱
=p

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

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

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


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

单元测试 #211

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

单元测试 #211

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

喜欢 (0)