问答中心分类: MULTITHREADING我应该如何对多线程代码进行单元测试?
0
匿名用户 提问 2小时 前

到目前为止,我已经避免了测试多线程代码的噩梦,因为它看起来像是一个雷区。我想问一下人们是如何测试依赖线程成功执行的代码的,或者人们是如何测试那些仅在两个线程以给定方式交互时才出现的问题的?
对于今天的程序员来说,这似乎是一个非常关键的问题,将我们的知识集中在这个恕我直言上会很有用。

Zach Burlingame 回复 2小时 前

我正在考虑就这个完全相同的问题发布一个问题。虽然威尔在下面提出了许多好的观点,但我认为我们可以做得更好。我同意没有单一的“方法”可以干净地处理这个问题。然而,“尽你所能进行测试”将标准设置得非常低。我会带着我的发现回来。

Zach Burlingame 回复 2小时 前

在 Java 中:包 java.util.concurrent 包含一些众所周知的类,它们可能有助于编写确定性 JUnit-Tests。看一下 –倒计时锁存器信号交换器

Zach Burlingame 回复 2小时 前

你能提供一个链接到你以前的单元测试相关问题吗?

Zach Burlingame 回复 2小时 前

@安德鲁·格林:stackoverflow.com/questions/11060/…

Zach Burlingame 回复 2小时 前

我认为重要的是要注意这个问题已有 8 年历史了,与此同时,应用程序库已经走了很长一段路。在“现代”(2016 年)中,多线程开发主要出现在嵌入式系统中。但是,如果您正在使用桌面或手机应用程序,请先探索替代方案。 .NET 等应用程序环境现在包括管理或大大简化大约 90% 的常见多线程场景的工具。 (异步/等待、PLinq、IObservable、TPL…)。多线程代码很难。如果你不重新发明轮子,你就不必重新测试它。

Zach Burlingame 回复 2小时 前

这可能是不受欢迎的想法,但如果你用 Rust 编写代码而不使用unsafe块,编译器实际上可以保证无竞争线程安全。对我来说,这是 Rust 的一个比通常提到的内存安全更重要的特性。

26 Answers
0
user1228user1228 回答 2小时 前

看,没有简单的方法可以做到这一点。我正在研究一个本质上是多线程的项目。事件来自操作系统,我必须同时处理它们。
处理测试复杂的多线程应用程序代码的最简单方法是:如果测试太复杂,则说明您做错了。如果您有一个有多个线程作用于它的单个实例,并且您无法测试这些线程相互交叉的情况,那么您的设计需要重做。它既简单又复杂。
有许多多线程编程方法可以避免线程同时通过实例运行。最简单的方法是使所有对象不可变。当然,这通常是不可能的。因此,您必须在设计中识别线程与同一实例交互的那些位置,并减少这些位置的数量。通过这样做,您可以隔离一些实际发生多线程的类,从而降低测试系统的整体复杂性。
但是您必须意识到,即使这样做,您仍然无法测试两个线程相互踩踏的所有情况。为此,您必须在同一个测试中同时运行两个线程,然后准确控制它们在任何给定时刻正在执行的行。你能做的最好的就是模拟这种情况。但这可能需要您专门为测试编写代码,而这充其量只是迈向真正解决方案的一半。
测试代码是否存在线程问题的最佳方法可能是通过代码的静态分析。如果您的线程代码不遵循有限的线程安全模式集,那么您可能会遇到问题。我相信 VS 中的代码分析确实包含一些线程知识,但可能不多。
看,就目前的情况来看(并且可能会在未来的好时机出现),测试多线程应用程序的最佳方法是尽可能降低线程代码的复杂性。尽量减少线程交互的区域,尽可能进行测试,并使用代码分析来识别危险区域。

Zombies 回复 2小时 前

如果您处理允许它的语言/框架,代码分析会很棒。 EG:Findbugs 会发现非常简单易用的静态变量共享并发问题。它找不到的是单例设计模式,它假设所有对象都可以多次创建。这个插件对于像 Spring 这样的框架来说是非常不合适的。

dsannella 回复 2小时 前

线程安全(contemplateltd.com/threadsafe, 免费试用) 是一个专门关注 Java 并发性的静态分析器。它在发现并发问题方面比 FindBugs 做得更好。看infoq.com/articles/…有关它在包括 Apache JMeter 和 K9Mail 在内的开源应用程序中发现的并发错误的示例。 (披露:ThreadSafe 是一种商业工具,我是生产它的公司 Contemplate 的联合创始人。)

user1228 回复 2小时 前

@DonSannella:(披露:我们在这里确实有自我推销的限制)

Dill 回复 2小时 前

实际上有一种治疗方法:活动对象。drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…

Bryan Rayner 回复 2小时 前

虽然这是一个很好的建议,但我仍然在问,“我如何测试那些需要多个线程的最小区域?”

Ronna 回复 2小时 前

“如果它太复杂而无法测试,那你就做错了”——我们都必须深入研究我们没有编写的遗留代码。这个观察结果对任何人有什么帮助?

Warren Dew 回复 2小时 前

静态分析可能是个好主意,但它不是测试。这篇文章真的没有回答这个问题,即如何测试。

Yarek T 回复 2小时 前

说得好。补充:如果您成功地保持设计对测试友好,编写测试的最佳方法是模拟调度程序并序列化将在野外受到冲击的案例的指令。更少的案例 = 更少的测试 = 健壮的代码。

0
Theo Lenndorff 回答 2小时 前

这个问题已经有一段时间了,但仍然没有回答……
kleolb02的回答很好。我会尝试更详细的。
有一种方法,我为 C# 代码练习。对于单元测试,您应该能够编程可重现的测试,这是多线程代码中最大的挑战。所以我的答案旨在强制异步代码进入测试工具,它可以工作同步的.
这是 Gerard Meszaros 书中的一个想法“xUnit 测试模式”并且被称为“Humble Object”(第 695 页):您必须将核心逻辑代码和任何闻起来像异步代码的东西彼此分开。这将产生一个用于核心逻辑的类,该类有效同步的.
这使您可以在一个环境中测试核心逻辑代码同步方法。您可以绝对控制您在核心逻辑上执行的调用时间,因此可以进行可重现的测试。这是分离核心逻辑和异步逻辑的收获。
这个核心逻辑需要被另一个类包裹起来,该类负责异步接收对核心逻辑的调用,并代表这些对核心逻辑的调用。生产代码只能通过该类访问核心逻辑。因为这个类应该只委托调用,所以它是一个非常“愚蠢”的类,没有太多逻辑。因此,您可以将这个异步工作类的单元测试保持在最低限度。
任何高于此(测试类之间的交互)都是组件测试。同样在这种情况下,如果您坚持“Humble Object”模式,您应该能够绝对控制时间。

CopperCash 回复 2小时 前

但有时如果线程之间相互配合良好,也应该进行一些测试,对吧?在阅读您的答案后,我绝对会将核心逻辑与异步部分分开。但我仍然会通过异步接口和 work-on-all-threads-have-been-done 回调来测试逻辑。

Nicolas Bousquet 回复 2小时 前

这对于具有某种形式的并发但并不真正相互交互的单线程程序和算法来说似乎很棒。我不认为它会很好地测试一个真正的并行算法。

Mikko Rantalainen 回复 2小时 前

这种测试也无法帮助您解决可能出现的死锁问题,其中多个线程以不同的顺序获取多个锁,并最终在继续之前相互等待对方释放锁。通用的解决方案是在所有线程中以相同的顺序获取所有需要的锁,这可以通过单元测试来验证。

0
David Joyner 回答 2小时 前

确实是硬汉!在我的 (C++) 单元测试中,我按照所使用的并发模式将其分为几个类别:

  1. 对在单线程中运行且不支持线程的类进行单元测试——简单,像往常一样测试。
  2. 单元测试监控对象(在调用者的控制线程中执行同步方法的那些)公开同步的公共 API——实例化多个执行 API 的模拟线程。构建锻炼被动对象内部条件的场景。包括一个运行时间较长的测试,它基本上可以在很长一段时间内从多个线程中击败它。我知道这是不科学的,但它确实建立了信心。
  3. 单元测试活动对象(那些封装了自己的线程或控制线程的那些)-类似于上面的#2,但根据类设计而有所不同。公共 API 可能是阻塞的或非阻塞的,调用者可能会获得期货,数据可能会到达队列或需要出队。这里有许多可能的组合;白盒子走了。仍然需要多个模拟线程来调用被测对象。

作为旁白:
在我做的内部开发人员培训中,我教并发支柱并将这两种模式作为思考和分解并发问题的主要框架。显然有更高级的概念,但我发现这套基础知识有助于让工程师远离困境。如上所述,它还导致代码更易于单元测试。

0
Warren Dew 回答 2小时 前

近年来,在为多个项目编写线程处理代码时,我曾多次遇到过这个问题。我提供了一个较晚的答案,因为大多数其他答案虽然提供了替代方案,但实际上并没有回答有关测试的问题。我的回答是针对没有多线程代码替代方案的情况。为了完整性,我确实涵盖了代码设计问题,但也讨论了单元测试。
编写可测试的多线程代码
首先要做的是将您的生产线程处理代码与所有进行实际数据处理的代码分开。这样,数据处理可以作为单线程代码进行测试,而多线程代码所做的唯一事情就是协调线程。
要记住的第二件事是多线程代码中的错误是概率性的。最不常出现的错误是会潜入生产中的错误,即使在生产中也难以重现,因此会导致最大的问题。出于这个原因,快速编写代码然后调试它直到它工作的标准编码方法对于多线程代码来说是一个坏主意。它将导致代码中容易的错误被修复而危险的错误仍然存​​在。
相反,在编写多线程代码时,您必须以一开始就避免编写错误的态度编写代码。如果你已经正确删除了数据处理代码,线程处理代码应该足够小——最好是几行,最坏是几十行——这样你就有机会在不写错误的情况下编写它,当然也不会写很多错误,如果您了解线程,请慢慢来,并且要小心。
为多线程代码编写单元测试
一旦尽可能仔细地编写多线程代码,仍然值得为该代码编写测试。测试的主要目的不是测试高度依赖于时间的竞争条件错误——不可能重复测试这种竞争条件——而是测试你防止此类错误的锁定策略是否允许多个线程按预期进行交互.
要正确测试正确的锁定行为,测试必须启动多个线程。为了使测试可重复,我们希望线程之间的交互以可预测的顺序发生。我们不想在测试中对线程进行外部同步,因为这将掩盖生产中可能发生的线程未外部同步的错误。这就留下了线程同步的时间延迟的使用,这是我在必须编写多线程代码测试时成功使用的技术。
如果延迟太短,那么测试就会变得脆弱,因为微小的时间差异——比如可能运行测试的不同机器之间——可能会导致时间关闭和测试失败。我通常做的是从导致测试失败的延迟开始,增加延迟以便测试在我的开发机器上可靠地通过,然后将延迟加倍,这样测试就有很大的机会在其他机器上通过。这确实意味着测试将花费大量时间,尽管根据我的经验,仔细的测试设计可以将时间限制在不超过十几秒。由于您的应用程序中不应该有很多地方需要线程协调代码,因此您的测试套件应该可以接受。
最后,跟踪您的测试捕获的错误数量。如果您的测试有 80% 的代码覆盖率,那么它有望捕获大约 80% 的错误。如果您的测试设计良好但没有发现任何错误,那么您很有可能没有其他只会出现在生产环境中的错误。如果测试发现一两个错误,您可能仍然很幸运。除此之外,您可能需要考虑仔细审查甚至完全重写您的线程处理代码,因为代码可能仍然包含隐藏的错误,这些错误在代码投入生产之前很难找到,而且非常那时很难修复。

Paul Williams 回复 2小时 前

测试只能揭示错误的存在,而不是它们的缺失。最初的问题是关于 2 线程问题,在这种情况下,可能会进行详尽的测试,但通常情况并非如此。对于最简单的场景之外的任何事情,您可能不得不咬紧牙关并使用正式的方法 – 但不要跳过单元测试!编写正确的多线程代码首先是困难的,但同样困难的问题是在未来防止回归。

prash 回复 2小时 前

最不为人知的方式之一的惊人总结。你的回答是对人们普遍忽视的真正隔离的打击。

Toby Speight 回复 2小时 前

十几秒是相当长的时间,即使你只有几百个这样长度的测试……

Warren Dew 回复 2小时 前

@TobySpeight 与普通单元测试相比,测试时间较长。我发现,如果线程代码设计得尽可能简单,那么六个测试就绰绰有余了——需要几百个多线程测试几乎肯定会表明线程安排过于复杂。

Toby Speight 回复 2小时 前

这是一个很好的论据,可以让你的线程逻辑尽可能地与功能分离(我知道,说起来容易做起来难)。并且,如果可能的话,将测试套件分解为“每次更改”和“预提交”集(这样您的每分钟测试不会受到太大影响)。

user625488 回复 2小时 前

错误计数相关性的代码覆盖率不适用于 TDD – 在每个红-绿-重构周期之后,您的所有测试都是绿色的。以这种方式处理单元测试错过了最重要的事情。单元测试应该是被测组件的权威规范。这就是编写单元测试的目的。如果它们不能用作文档,它们就是垃圾。如果它们难以用文档来表述,那就是代码恶臭。

Warren Dew 回复 2小时 前

@user625488 确实适用于测试驱动开发,但 TDD 不能有效地用于线程处理代码。幸运的是,即使在 TDD 环境之外,单元测试也很有用。

user625488 回复 2小时 前

@WarrenDew 确保 TDD 可用于您在上面描述的专用线程处理组件 – 我已经反复做过。但是,根据测试代码的性质,此类测试可能不太相关。如果您在编写生产代码后编写单元测试,它们确实也很有用 – 但更不用说。在事后编写单元测试时,还有一个风险:你冒着测试你实现的东西的风险,而不是你测试的东西应该已实施。即,您可能会被您创建的关于实际测试要求的实现所蒙蔽。

Mikko Rantalainen 回复 2小时 前

获得 100% 的代码覆盖率只是自动化测试的开始。一旦你有了它,你也想开始突变测试,以了解你的测试实际上有多好或多差。

0
ollifant 回答 2小时 前

我在测试多线程代码时也遇到了严重的问题。然后我在 Gerard Meszaros 的“xUnit 测试模式”中找到了一个非常酷的解决方案。他描述的模式被称为卑微的对象.
基本上,它描述了如何将逻辑提取到一个单独的、易于测试的组件中,该组件与其环境分离。在你测试了这个逻辑之后,你可以测试复杂的行为(多线程、异步执行等……)