您将如何对内存分配器进行单元测试?

Posted

技术标签:

【中文标题】您将如何对内存分配器进行单元测试?【英文标题】:How would you unittest a memory allocator? 【发布时间】:2008-09-23 06:36:32 【问题描述】:

今天有很多人将单元测试作为开发的基础进行销售。这甚至可能适用于面向算法的强例程。但是,您将如何进行单元测试,例如内存分配器(想想 malloc()/realloc()/free())。生成满足指定接口的有效(但绝对无用)内存分配器并不难。但是如何为单元测试功能提供适当的上下文,这是绝对需要的,但不是合同的一部分:合并空闲块,在下一次分配时重用空闲块,将多余的空闲内存返回给系统,断言分配策略(例如first-fit) 确实受到尊重,等等。

我的经验是断言,即使复杂且耗时(例如遍历整个空闲列表以检查不变量)也比单元测试要少得多,而且比单元测试更可靠,尤其是。在编写复杂的时间相关算法时。

有什么想法吗?

【问题讨论】:

【参考方案1】:

高度可测试的代码的结构往往与其他代码不同。

您描述了您希望分配器执行的几项任务:

合并空闲块 在下一个重用空闲块 分配 将多余的可用内存返回给 系统 断言分配策略 (例如首次拟合)确实受到尊重

虽然您可能将分配代码编写得非常耦合,例如在一个函数体中执行其中几项操作,但您也可以将每个任务分解为可测试的代码块。这几乎是您可能习惯的反转。我发现可测试的代码往往非常透明,并且由更多的小​​块组成。

接下来,我想说的是,任何类型的自动化测试都比没有自动化测试要好。我肯定会更多地关注确保你的测试做一些有用的事情,而不是担心你是否正确使用了模拟,你是否确保它被正确隔离以及它是否是一个真正的单元测试。这些都是令人钦佩的目标,有望使 99% 的测试变得更好。另一方面,请使用常识和您最好的工程判断来完成工作。

如果没有代码示例,我想我不能更具体。

【讨论】:

【参考方案2】:

如果其中有任何逻辑,可以对其进行单元测试。 如果您的逻辑涉及做出决策和调用操作系统/硬件/系统 API,则伪造/模拟设备相关调用并对您的逻辑进行单元测试,以验证是否在给定的一组前提条件下做出了正确的决策。在单元测试中遵循 Arrange-Act-Assert 三元组。 断言不能替代自动化单元测试。它们不会告诉你哪个场景失败了,它们不会在开发过程中提供反馈,它们不能用来证明代码满足所有规范等等。

非模糊更新: 我不知道确切的方法调用..我想我会“自己动手” 假设您的代码检查当前条件,做出决定并根据需要调用操作系统。假设您的操作系统调用是(您可能有更多):

void* AllocateMemory(int size); 
bool FreeMemory(void* handle);
int MemoryAvailable();

首先把它变成一个接口,I_OS_MemoryFacade。创建此接口的实现以对操作系统进行实际调用。现在让你的代码使用这个接口——你现在已经将你的代码/逻辑与设备/操作系统解耦了。接下来在你的单元测试中,你使用一个模拟框架(它的目的是给你一个指定接口的模拟实现。然后你可以告诉模拟框架期望进行这些调用,用这些参数并在创建时返回。在测试结束时,您可以要求模拟框架验证是否满足所有期望。(例如,在此测试中,应调用 AllocateMemory 三次,参数为 10、30、50然后是 3 次 FreeMemory 调用。检查 MemoryAvailable 是否返回初始值。) 由于您的代码依赖于接口,因此它不知道真实实现与您用于测试的假/模拟实现之间的区别。 谷歌出“模拟框架”以获取更多信息。

【讨论】:

能否请您提供有关如何编写单元测试的更多详细信息。您大量提倡它们,但是我阅读了问题,然后阅读了您的回答,但我仍然不明白您的建议到底是什么。 你的回答太抽象了,没用。您能否举例说明如何对内存分配器进行单元测试?让我们更具体一点:first-fit,当空闲内存是分配内存的两倍时,它将 50% 的空闲块返回给系统。 第一步是让您的内存分配器通过接口而不是直接通过直接系统调用从系统获取和释放内存。第二步是创建一个可以在测试中使用的接口的模拟版本。第三步是测试和检查代码是如何使用接口的。 将我的答案扩展为“有意义”:)【参考方案3】:

这两件事都有自己的位置。使用单元测试来检查接口的行为是否符合预期,并使用断言来检查合同是否得到遵守。

【讨论】:

【参考方案4】:

您可能还希望包括性能测试、压力测试等。它们不会是单元测试,因为它会测试整个事情,但它们在内存分配器的情况下非常有价值。

单元测试不排除这些类型的测试。最好同时拥有它们。

【讨论】:

【参考方案5】:

我也认为单元测试被高估了。它们有其用处,但真正提高程序质量的是对其进行审查。另一方面,我真的很喜欢断言,但它们并不能取代单元测试。

我不是在谈论同行评审,而只是重新阅读您编写的内容,可能同时使用调试器单步执行并检查每一行是否完成了它应该做的事情,这将使软件质量飙升。

我建议测试一大块功能而不是微小的方法调用的“高级”单元测试。后者往往会使任何代码更改变得极其痛苦和昂贵。

【讨论】:

【参考方案6】:

就我个人而言,我发现大多数单元测试都像别人的愿望,而不是我的愿望。我认为任何单元测试都应该像普通程序一样编写,除了它除了测试库/算法或代码的任何部分之外什么都不做。

我的单元测试通常不使用CUnit、CppUnit 和类似软件之类的工具。 我创建自己的测试。例如,不久前,我需要在通常情况下测试容器的新实现是否存在内存泄漏。单元测试不足以提供一个好的测试。相反,我创建了自己的分配器,并使其在一定(可调整)数量的分配后无法分配内存,以查看我的应用程序在这些情况下是否存在内存泄漏(并且它有:))。

如何通过单元测试来做到这一点?更加努力地使您的代码适合单元测试“模式”。

所以我强烈建议不要每次都使用单元测试,因为它很“时髦”,但只有在很容易将它们与你喜欢测试的代码集成时才使用。

【讨论】:

【参考方案7】:

单元测试不仅仅是为了确保您的代码正常工作。这也是一种非常好的设计方法。如前所述,为了使测试有用,代码需要尽可能地解耦,例如在需要的地方使用接口。

我并不总是首先编写测试,但通常如果我在开始某事时遇到困难,我会编写一个简单的测试,试验设计并从那里开始。此外,好的单元测试可以作为好的文档。在工作中,当我需要查看如何使用特定类或类似类时,我会查看它的单元测试。

请记住,单元测试不是集成测试。单元测试确实有其局限性,但总的来说,我认为这是一个非常好的工具,可以知道如何正确使用。

【讨论】:

【参考方案8】:

因此,您遇到了一个问题,即您的分配器被测试框架使用,这可能会在您尝试测试时导致分配器状态出现问题。考虑为分配器函数添加前缀(请参阅dlmalloc)。你写

prefix_malloc();
prefix_free();

然后

#ifndef USE_PREFIX
#define prefix_malloc malloc
#define prefix_free free
#endif

现在,将您的构建系统设置为使用 -DUSE_PREFIX 编译库的一个版本。编写单元测试以调用 prefix_malloc 和 prefix_free。这使您可以将分配器的状态与系统分配器的状态分开。

如果您正在使用 sbrk 并且系统分配器使用 sbrk,那么如果任一分配器假定它可以完全控制断点,您可能会遇到麻烦。在这种情况下,您可能希望链接另一个分配器,您可以将其配置为仅使用 mmap,以便您的分配器可以有断点。

【讨论】:

以上是关于您将如何对内存分配器进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章

内存分配

java 内存简介

期末复习——内存管理

Golang 1.14中内存分配、清扫和内存回收

OSDev:为什么我的内存分配功能在AHCI初始化函数中突然停止工作?

JVM内存分配和常量池