几乎每个开发人员在他们的职业生涯中都遇到过(或构建过)一段令人“望而却步”的代码,甚至都没人愿意去碰它。 因为要对代码进行修改可能很容易,但如果代码中有太多的边界场景,那么就很容易破坏某些东西。
最终当然会出现由代码引起的bug。也没有人会愿意在那里碰运气。修复它可能很容易,但测试它需要几个小时。这是假设所有的边界情况都被记录下来了或者是已知的情况。(哈!)
自动化测试不会使这段代码变得多漂亮,但肯定会令它使用起来更加容易。对于一个需要几个小时手动完成的测试计划来说,假如使用一个自动化的测试套件,也许在几秒钟就可以运行完。这不仅节省了时间,而且可以给您信心去修改一些敏感代码。因为有了自动化集成测试,这件事变得更加容易了,只要您愿意,您甚至可以重写它们。
自动化测试包括很多主题。在这篇文章中,我们将涵盖以下内容:
- 单元测试
- 集成测试
- Selenium测试
- 自动测试的限制
单元测试
单元测试是自动化测试的最基本形式。这也是大多数人在提到编写测试时会谈论的内容。单元测试的优点是易于理解、易于编写和执行一致性。
假设您有一个将英寸换算到厘米的函数: 将英寸转换为厘米(英寸值)
以下是一个简单的单元测试用例,2英寸换算后应该是5.08厘米:
仅有一个单元测试是不够的,所以我们为边界场景添加了一些测试用例。
此外,还应该考虑异常处理,以确保我们的应用程序不会因为错误的输入而崩溃。
单元测试也适用于更复杂的函数。例如,Dynomantle通过抓取网页来提供链接预览。而网页上的描述可以通过多种方式提供。下面是一个从Twitter元标签中提取描述的测试:
此外,还有许多测试,其中的测试代码完全相同,但是html已被更改以适应不同的情况。
现在,单元测试对于包含所有所需逻辑的函数来说是非常出色的。这些函数不与代码库的其他部分交互。最重要的是,它们不与任何其他系统(如数据库)交互。
以Dynomantle为例。假设有一个用于添加书签的单元测试。要验证书签是否被正确添加,我们必须为注释去检索书签。第一次运行这个测试时,数据库中只有一个书签。但问题是,第二次运行测试时,我们将得到两个书签。第三次将得到三个书签。数据库会随着每次测试的执行而变化,这会破坏测试的一致性。
这个问题可以通过使用“mock”来避免。我们不会深入讨论mock,因为请注意,我们是在避免问题,而不是用mock来解决问题。尽管单元测试的目标是测试应用程序的一小部分,但是我们仍然需要测试整个应用程序。不同的部分如何相互作用是非常重要的。
下图很好地说明了单元测试的局限性:
来源:https://natooktesting.wordpress.com/2017/08/24/x-unit-tests-0-integration-tests/
遥控传感器可以通过人手正确触发,水龙头可以正确地出水,洗手盆要能将水准确地接住....而这一切需要水龙头和洗手盆能很好地相互配合才行。
集成测试
这就引出了集成测试的话题。这也是我们对系统进行更大范围的测试之处。如果您有一个调用5个函数的API端点,那么单元测试是去覆盖这5个函数中的每一个,而集成测试是会作为一个整体去运行API端点。
以Dynomantle为例,端点可以添加URL作为书签。单元测试涵盖用于解析HTML内容和检索链接预览所需数据的每个单独的函数。而集成测试调用端点本身,并验证链接预览作为一个整体是否正确显示。
这是对用户如何使用应用程序的一个更具代表性的测试。然而,与单元测试相比,集成测试常常被忽视,因为它们不容易编写,也不容易执行,而且很难保持一致性。
我们仍然存在这样的问题:每次添加书签时,数据库中的数据都会发生变化。因为很难与集成测试保持一致的执行,单元测试会被谈论得更频繁。为了确保一致性,Dynomantle的集成测试包括:
- 使用新模式设置数据库
- 创建新的搜索索引
- 创建测试用户
- 运行测试
- 删除数据库
- 删除搜索索引
- 清除Redis
这个列表只会随着新系统的添加或构建而增长。Dynomantle也有自己的便捷性。用户的所有数据都与该用户独立。这意味着每个测试都可以有自己的用户,使得并发运行测试变得很容易。同时,对依赖于汇总一组甚至所有的用户数据的情况而言,它在设置和执行方面会有更大的复杂性。
集成测试的复杂性使得这些形式的测试具有潜在的脆弱性。对开发人员来说,没有能比与他们的变更无关的测试失败更令人沮丧的了。那些错误的测试会令人不由得认为这种测试是不值得维护的,哪怕是对那些坚信测试的人来说也是如此。
使用时间戳的逻辑就是一个例子。仅仅在代码中需要的地方创建时间戳非常容易,但问题是:你的时间戳是精确到毫秒还是秒呢?集成测试的执行速度比人类用户与应用程序交互的速度要快。如果你的时间戳只精确到秒,那么你可以在同一秒创建多个时间戳。因此,任何按时间戳排序的东西现在都将变得不确定。任何关于排序顺序的测试现在也都有将有50%的时候会失败。
听起来,以毫秒为单位存储时间戳是一个简单的解决方法。不过,根据应用程序的不同,这可能会带来较大的存储或性能影响。当这个决定对您的用户无关时,您是否是仅仅为了运行测试而做出了这个决定?
您还可以有策略地决定在何处创建时间戳。它看起来是这样的:
其中:
这至少允许您在测试中使用您想要的任何时间戳来测试func1。在func1测试中,可以考虑到更多关于排序顺序的边缘情况,而api_endpoint测试可以稍微空闲一些。
但是,现在的代码有点复杂,因为您必须在不需要时间戳的地方也去创建时间戳,然后将它传递下去。在这个示例中,它看起来并没有那么糟糕,但是在任何实际的应用程序中,它都可能会非常恼人。
围绕这个问题有很多解决方案,但每个解决方案都有一些利弊。你仍然需要提前考虑这些问题。时间戳是很容易想到的例子,因为它们非常常用。但是,在集成测试中,每个应用程序都有自己的一组独特的问题需要处理。
构建可靠的集成测试需要时间,因为您必须致力于解决测试本身的bug。
尽管存在着挑战,但集成测试对于任何自动化测试策略而言都是关键部分。为了有信心在不依赖于全面手工回归测试的情况下进行发布,自动化测试需要像实际用户一样在场景中运行。这不是仅仅用单元测试就能来模拟的。
然而,集成测试并不是我们所能拥有的最高保真度测试。如果您有一个web应用程序,您可能会用Javascript为前端构建集成测试,并且其仅仅测试前端。同样地,你也会为你的后端进行集成测试,并且其仅仅测试后端。我们如何测试前端和后端是否能很好地交互呢?
Selenium Testing
Selenium测试是针对web应用程序的最高保真度的自动化测试。真实的浏览器由Selenium驱动程序启动,应用程序会像在用户浏览器中一样呈现,测试将以与用户相同的方式在应用程序中单击/拖动/键入。如果您的应用程序只有一个页面,这也无关紧要。Selenium将您的应用程序视为一个黑盒,除了html元素的id和类名之外,不需要知道其他任何东西。
好在您还可以观看测试的运行。与通过查看代码来重现失败的测试用例不同,您可以观察Selenium在应用程序中不停点击,直到它遇到失败用例为止。您甚至可以在测试失败时打开浏览器控制台并开始调试。
不过,您可能已经猜到了selenium测试的最大缺点:执行速度。Selenium的点击速度比人类用户快,但它仍要等待页面加载和API调用完成。有一个“headless”模式的选项,它使可视化浏览器不必呈现应用程序,但执行测试仍然需要一段时间。使用Selenium,执行一系列需要2分钟的集成测试可能需要数小时。即使您不手动进行测试,也需要等待很长时间。
将应用程序视为黑盒还意味着Selenium不适合测试应用程序的某些技术方面,比如数据是从缓存加载还是从数据库加载。
测试可能最适合测试应用程序中的最佳方法了,而大多数边界情况就需要留给集成测试来进行。
自动化测试的局限性
人工测试软件会涉及到很多方面。首先是视觉设计。您可以编写集成测试或Selenium测试,以确保按钮是可单击的。但它很难测试按钮是否足够大,是否可见,是否部分被其他可视组件覆盖,是否有适当的填充,甚至颜色是否正确。可能有种办法是通过使用屏幕截图,并验证测试执行的结果是否具有与屏幕截图相同的屏幕。然而,这些测试可能不够健壮,特别是当您频繁地在UI上迭代时。
旁注:如果你知道有什么可靠的方法来克服这个限制,请发送消息或推特到https://twitter.com/dynomantle
自动化测试的另一个限制是任何涉及第三方服务的东西。仅仅依靠AWS或Google云是一方面。这些服务是为规模化和自动化测试而构建的。但是,如果你的产品是用来处理电子邮件,那么自动化测试在其中看起来非常像垃圾邮件。如果你的产品是用来抓取web页面,那么自动测试可能看起来像拒绝服务攻击。如果您的产品依赖于另一个服务的API,那么自动化测试可以很快就会达到API的限制。
最后,虽然自动化测试可以尝试模拟用户的动作,但测试只能执行您让它们执行的操作。用户对软件的非预期使用的思考能力是显然不足的。对于开发一个产品来说,这是一种幸事。但对于测试一个产品来说,这是一个挑战。您仍然会遇到bug,并且在修复这些bug时,您必须保持对测试的更新。
更多例子
希望这篇文章能够帮助您开始制定您自己的自动化测试策略。如果你想了解如何在ReactJS/Typescript和Python中创建测试的详细说明,请注册Dynomantle来访问这些内容吧。
{测试窝原创译文,译者:Elaine66}