编写良好的单元测试:循序渐进教程 正面案例 极端案例 负面案例 ExampleUnitTests 教程总结

2025-05-25

编写良好的单元测试:分步教程

阳性病例

极端情况

负面案例

总结

示例单元测试

教程

这篇文章最初发表在我的博客smartpuffin.com上。


假设我们刚刚编写了一个计算地球上两点之间距离的方法。我们想尽可能地测试它。那么,我们该如何设计测试用例呢?我们到底需要测试什么呢?

额外奖励:了解有关斐济群岛的惊人事实。🇫🇯

先决条件

我假设你已经熟悉单元测试的概念。我将使用 Java 和 JUnit,但如果你不熟悉它们也不用担心:本教程的目的是学习如何编写更完整的测试套件。你将逐步掌握特定于平台的知识。

这是我存放本文完整源代码的仓库链接。您可以下载并在阅读时查看,也可以在阅读结束后再查看。

这是我之前关于单元测试主题的文章,更广泛地解释了单元测试的最佳实践

我们走吧!

阳性病例

让我们首先将我们的方法视为一个黑匣子。

我们有这个 API:

// 使用半正矢公式返回以米为单位的距离
double getDistance(double 纬度1, double 经度1, double 纬度2, double 经度2);

我们可以测试什么?

让自己处于测试心态

你现在需要从开发人员转型为测试员。这意味着你必须停止相信自己。如果你不测试代码,你就无法知道它是否正常工作!

如果您手动测试它,您就不知道下次有人对您的代码或接近代码进行更改时它是否会起作用。

此外,您可以确信,无论您在这个经过充分测试的代码上构建什么代码,您都无需担心任何问题。

当你的代码经过充分测试时,你会感到无比安全。你可以自由地重构并进一步开发新功能,同时确保旧代码不会崩溃。

如果你发现了虫子,就答应犒劳自己一下。但一定要小心:吃太多饼干有害健康。

第一次测试

好了,我们准备测试了!首先应该测试什么?

我首先想到的是:

计算两点之间的距离,并与手动计算的真实实数进行比较。如果匹配,则一切正常!

这是一个很好的测试策略。让我们为此编写一些测试。

我会选几个点:一个在荷兰最北端,一个在最南端。实际上,我想从上到下测量一下这个国家。

我点击了地图上的几个点,得到了这个:

点 1:纬度 = 53.478612,经度 = 6.250578。
点 2:纬度 = 50.752342,经度 = 5.916981。

我使用外部网站计算了距离,结果是304001.021046米。这意味着荷兰长304公里!

现在,我们如何使用这个?

关于域名区域的思考

我们需要考虑特定领域的具体问题。并非所有问题都能从代码中清晰地看出:有些问题取决于领域,有些则取决于未来的计划。

在我们的示例中,返回的距离以米为单位,并且是双精度数。

我们知道双精度数容易出现舍入误差。此外,我们也知道,本次计算中使用的方法——半正矢公式——对于我们这个极其复杂的星球来说,有些不精确。对于我们目前的应用来说,它已经足够了,但或许,我们以后会想用更高精度的计算来替代它。

所有这些都让我们想到,我们应该将计算值与预期值进行精确的比较。

让我们选择一个适合我们域面积的值。为此,我们需要再次回顾一下我们的域面积和要求。1毫米可以吗?可能可以。

这是我们的第一次测试!

double Precision = 0.001; // 我们使用 1mm 精度比较计算距离
...
@测试
void distanceTheNetherlandsNorthToSouth() {
    双倍距离=GeometryHelpers.getDistance(53.478612,6.250578,50.752342,5.916981);
    断言等于(304001.0210,距离,精度);
}

让我们运行这个测试并确保它通过。好极了!

更多测试

我们计算了横跨荷兰的距离。当然,反复核对也是个好主意。

您的测试应该有所不同 - 这有助于您为这些讨厌的错误撒下更大的网!

让我们再找一些有趣的点。我们来了解一下澳大利亚有多宽吧?

@测试
无效距离澳大利亚西部到东部(){
    双倍距离=GeometryHelpers.getDistance(-23.939607, 113.585605, -28.293166, 153.718989);
    断言等于(4018083.0398,距离,精度);
}
或者从 开普敦 到 约翰内斯堡 有多远?
@测试
void distanceFromCapetownToJohannesburg() {
    双倍距离=GeometryHelpers.getDistance(-33.926510, 18.364603,-26.208450, 28.040572);
    断言等于(1265065.6094,距离,精度);
}
现在我们来思考一下:我们能确定距离与计算方向无关吗?我们也来测试一下!
@测试
如果在两个方向测量距离相同,则无效()
    // 测试无论我们测量哪个方向的距离都是相同的
    双距离方向1 = GeometryHelpers.getDistance(-33.926510, 18.364603,-26.208450, 28.040572);
    double distanceDirection2 = GeometryHelpers.getDistance(-26.208450, 28.040572, -33.926510, 18.364603);
    断言等于(1265065.6094,距离方向1,精度);
    断言等于(1265065.6094,距离方向2,精度);
}

极端情况

太好了,我们已经介绍了一些基本情况——该方法的主要功能似乎可以发挥作用。

我们现在可以吃一块饼干了——因为我们已经完成了一半。🍪

现在,让我们将代码扩展到极限!

考虑领域问题再次有所帮助。

我们的星球与其说是平面,不如说是一个球体。这意味着纬度在 180 度和 -180 度之间有一个区域,留下了一条“缝隙”,我们需要格外小心。这个区域周围的数学运算经常出现错误。我们的代码处理得好吗?

一个好主意就是为它编写一个测试,你不觉得吗?

斐济群岛周围有两个点的地图截图
需要与 Geo 合作吗?请务必联系斐济!

@测试
void distanceAround180thMeridianFiji() {
    双倍距离=GeometryHelpers.getDistance(-17.947826, 177.221232, -16.603513, -179.779055);
    断言等于(351826.7740,距离,精度);
}
经过进一步思考,我发现了一个更棘手的情况——两个具有相同纬度的点之间的距离;但一个点的经度为 180,另一个点的经度为 -180。
双倍距离 = GeometryHelpers.getDistance(20, -180, 20, 180);
// 我的读者,我想问您一个问题:这个距离是多少?
好了,那个奇怪的180度子午线我们已经搞定了。不过,0度子午线我们肯定知道……对吧?对吧?我们也测试一下吧。
@测试
无效距离围绕0thMeridianLondon(){
    双倍距离=GeometryHelpers.getDistance(51.512722,-0.288552,51.516100,0.068025);
    断言等于(24677.4562,距离,精度);
}
我们还能想到什么其他情况呢?我想了想,列出了以下这些:
  • 那么两极呢?我们来计算一下北极和南极洲的距离。
  • 从一个极点到另一个极点怎么样?
  • 地球上的最大距离是多少?
  • 那么距离真的很小吗?
  • 如果两个点完全相同,我们得到的距离是 0 米吗?
我不会在这里添加所有这些测试,因为那样太占空间了。你可以在代码库中找到它们。你懂的。试着发挥你的创造力。这是一个尽可能多地提出想法的游戏。不要害怕“想法太多”。代码永远不会“测试得太好了”。

里面有什么

此时,查看代码内部也会有所帮助。您是否已完全覆盖?代码中可能有一些“if”和“else”语句可以给您一些提示?请谷歌搜索“半正矢公式”,看看它的局限性。我们已经了解了精度。还有其他什么地方可能会出错吗?是否存在一些参数组合会导致代码返回无效值?集思广益,想想吧。

负面案例

负面情况是指方法应该拒绝执行其工作的情况。这是一种可控的失败!再次强调,我们应该记住定义域。纬度和经度是特殊值。纬度应该精确地定义在 [-90, 90] 度范围内。经度则在 [-180, 180] 范围内。这意味着,如果我们传递了无效值,我们的方法就会抛出异常。让我们为此添加一些测试!
@测试
void invalidLatitude1TooMuch() {
    断言抛出(IllegalArgumentException.class,()-> {
        GeometryHelpers.获取距离(666,0,0,0);
    });
}
@测试
void invalidLatitude1TooLittle() {
    断言抛出(IllegalArgumentException.class,()-> {
        GeometryHelpers.获取距离(-666, 0, 0, 0);
    });
}

我们将为纬度 2 和两个经度参数添加相同的测试。

我们这里写的是 Java,double 类型的参数不能为空。不过,如果你使用的语言允许,那就测试一下吧!

如果您使用的是动态类型语言,并且可以传递字符串而不是数字,请对其进行测试!

补充一点:动态类型语言比静态类型语言需要更广泛的测试。对于后者,编译器会处理很多事情。而对于前者,一切都掌握在你手中。

总结

您可以在此处查看并下载本教程的源代码。查看并尝试提出一些更有用的测试场景。

示例单元测试

这是有关编写良好单元测试套件的教程的源代码。

教程

请参阅此处的教程:

http://smartpuffin.com/unit-tests-tutorial/

如何处理此问题

  1. 打开教程。
  2. 打开GeometryHelpers.java
  3. 打开GeometryHelpersTest1.java。查看测试结构。运行测试。
  4. 打开GeometryHelpersTest2.java
  5. 将中的第一个测试GeometryHelpersTest1.java与中的第一个测试进行比较GeometryHelpersTest2.java。看看即使是最小的领域知识也很重要吗?
  6. 想想你还能测试什么。我肯定我漏掉了什么!
  7. 在您自己的项目中应用最佳实践。
  8. 喜欢本教程吗?分享、点赞、订阅、说声谢谢吧。我很高兴知道!

如何运行测试

我使用 Intellij IDEA 和 Java 1.8+ 运行它。

  1. 下载源代码。
  2. 在 Intellij IDEA 中打开包含代码的文件夹。
  3. 打开文件GeometryHelpersTest1.java。你会看到行号旁边有绿色的“运行测试”按钮。点击下一个……

以下是一些后续阅读材料,用于了解单元测试的最佳实践。

对于你自己的项目,选择最能从测试中获益的部分,并尝试用测试覆盖它。发挥你的创造力!把它当成一个挑战,一场与自己的较量!

单元测试很有趣!

文章来源:https://dev.to/itnext/writing-good-unit-tests-a-step-by-step-tutorial-1l4f
PREV
2021 年作为 MERN Stack 开发人员我将学习的技术(附资源)
NEXT
为什么你应该学习递归