持续集成/验证软件开发
在计算机编程中,单元测试是一种方法,通过该方法测试源代码的各个单元以确定它们是否适合使用。单元是应用程序中最小的可测试部分。在过程式编程中,单元可以是单个函数或过程。在面向对象编程中,单元通常是方法。单元测试由程序员或偶尔由白盒测试人员在开发过程中创建。在 Java 世界中,我们有许多流行的选择来实现单元测试,JUnit 和 TestNG 可以说是最流行的选择。本文中提供的示例将使用 TestNG 语法和注释。
传统上(“传统”是指它们相对简短的历史),单元测试被认为是非常简单的测试,用于验证软件方法的基本输入和输出。虽然这可能是真的,并且这种简单的测试可以提供一定价值,但单元测试可以实现更多。事实上,不仅可能,而且建议我们在单元测试框架内实现我们的大部分用户验收、功能以及可能的一些非功能测试。为了进一步提高质量,我们可以用单元测试来增强验收。[6]Dean Leffingwell,敏捷软件需求虽然我个人从未成为测试驱动开发的粉丝(我认为测试驱动开发所需的假设不允许真正的迭代方法),但我确实相信与开发并行创建单元测试会产生更高质量的软件。在敏捷的世界中,这意味着在没有相应的单元测试的情况下,任何功能需求(或用户故事)都不被视为完全实现。这种对单元测试的严格看法可能有点极端,但并非毫无道理。
开发人员可能编写的第一个单元测试可能非常简单,以至于几乎毫无用处。它可能看起来像这样。
给定一个方法
public int doSomething (int a, int b) { … return c;}
一个简单的单元测试可能看起来像这样
public class MyUnitTests { @Test public void testDoSomething() { assertEquals(doSomething(1, 2), expectedResult); }}
给定一个非常简单的方法,开发人员能够断言,本质上,a + b = c。这很容易编写,并且几乎没有开销,但它实际上不是一个非常有用的单元测试。
早期尝试自动化功能测试
很久以前,我参与了一个项目,管理层在该项目中投入了大量时间和培训来尝试实施自动化测试。所选工具是 Rational Robot™(现为 IBM 产品)。Robot 等工具背后的理念是,测试创建者可以录制测试宏,记录验证点并稍后重播宏,并记录测试结果。Rational Robot 和 WinRunner 等工具试图用录制脚本替换人工测试人员。这些自动化脚本可以使用脚本语言编写,或者更常见的是通过记录鼠标移动、单击和键盘操作来编写。在这方面,这些测试自动化工具通过用户界面允许黑盒测试。
在这个过度简化的自动化测试视图中,测试实现存在太多后勤问题,以至于不切实际。对用户界面进行任何微小的更改都可能导致测试脚本崩溃。负责维护这些自动化脚本的人员经常发现自己花费更多时间维护测试,而不是将它们用于实际的应用程序测试。
Rational Robot 及其类似工具依然存在,但我指的是它们过去时态,因为在我看来,此类工具已被证明是失败的。我这么说,是因为我个人花了很多时间在这样的工具中创建自动化脚本,而且我后来很沮丧地得知它们不会被使用,因为随着项目进展,大量界面代码会发生变化。此类变化是完全预期的,然而,录制的自动化测试并不适合迭代开发环境或正在进行的项目。
使用单元测试框架自动化功能测试
大多数软件项目,尤其是在任何敏捷环境中,都会经历频繁的更改和重构。如果传统的单流程瀑布模型有效,那么前面提到的那种录制测试脚本可能会正常工作,尽管好处很小。
但现在应该众所周知的是,传统的单流程瀑布模型已经失败,我们生活在一个迭代/敏捷的世界中。因此,我们的自动化测试必须同样适合持续变化。由于功能单元测试与白盒和黑盒级别的需求密切相关,因此开发人员,而不是测试人员,在创建自动化测试中起着不可或缺的作用。
为了实现这种级别的单元测试,必须有一个测试框架。这需要一些前期工作,创建此类框架的细节超出了本文的范围。此外,测试框架的需求会因项目而异。
测试夹具成为复杂功能单元测试的重要组成部分。测试夹具是一个类,它包含运行此类单元测试所需的所有设置。它提供可以创建通用对象的方法(例如,测试服务器和模拟接口)。测试夹具中包含的详细信息特定于每个项目,但一些常见方法包括测试设置、模拟和模拟对象创建和销毁,以及声明任何要在所有单元测试中使用的通用功能。为了更详细地介绍测试夹具的创建,需要更多信息,这里无法提供。
鉴于在创建复杂单元测试时看似极端的开销,我们可能会开始质疑其价值。毫无疑问,创建一个用途广泛且有用的单元测试框架(包括测试夹具,它包含模拟运行环境以进行测试所需的所有必要对象和设置)需要大量前期成本。鉴于人工功能和用户验收测试仍然是项目的必要条件,似乎可能存在工作重叠。
但事实并非如此。
通过对一个可靠的单元测试框架进行一些前期创建,我们可以努力使创建单元测试变得简单。我们甚至可以要求在允许任何功能需求实现(或工单)被视为完成之前,为其创建单元测试。此外,当我们发现潜在的功能问题时,我们有机会立即引入新的测试!“下面讨论的硬件系统、软件程序和一般质量保证系统控制对于医疗器械的自动化制造至关重要。软件和相关设备的系统验证将确保符合 QS 规范;并减少混淆,提高员工士气,降低成本,提高质量。此外,适当的验证将使自动化生产和质量保证设备顺利地整合到生产运营中。医疗器械及其生产所使用的制造过程从简单到非常复杂。因此,QS 规范需要并且是一个灵活的质量体系。随着越来越多的设备制造商转向自动化生产、测试/检验和记录系统,这种灵活性是宝贵的。[1]
什么是好的单元测试?
托马斯·H·法里斯在其著作《安全可靠的软件》中描述了单元测试:软件测试可以在软件模块或单元完成时进行。单元测试对于在单元完成时测试单元非常有效,此时其他单元或组件尚未完成。测试仍然需要完成,以确保应用程序在所有软件单元或组件一起执行时按预期工作 [2]。这是一个开始,但单元测试可以实现更多!法里斯继续分解出许多不同类别的软件 [3]
- 黑盒测试
- 单元测试
- 集成测试
- 系统测试
- 负载测试
- 回归测试
- 基于需求的测试
- 基于代码的测试
- 基于风险的测试
- 临床测试
传统上,这可能是一个合理的分解。但是,如果明智地使用,并在适当的框架下,我们可以使用模拟真实生产环境的有效单元测试来执行黑盒测试、集成测试、系统测试、负载测试、回归测试、基于需求的测试、基于代码的测试、基于风险的测试和临床测试。本文的目的是不深入探讨如何技术细节(解释单元测试框架、夹具、模拟对象和模拟将需要更多空间)。相反,我只想指出由此产生的好处。为了实现这些好处,您的软件团队需要深入了解单元测试。这需要一些时间,但这将是非常值得的。
让单元测试超越我们传统意义上的“单元测试”,并更进一步,自动化功能测试是一个好主意)。这是另一个团队成员经常(错误地)觉得没有足够时间完成所有工作的领域。
正如 Harris 所言:软件测试和缺陷修复非常耗时,通常会占用软件组织所有工作量的超过一半 [3]。测试不必等到整个产品完成才开始;在每个代码迭代完成后,可以对迭代设计和开发的代码进行测试。在开始验证之前,项目计划或其他测试计划文档应讨论总体策略,包括要执行的测试类型、要执行的具体功能测试,以及指定测试目标,以确定产品何时准备充分以供发布和分发 [4]。Harris 指出在我们受 FDA 监管的环境中非常重要的一点是,我们必须记录和描述我们的测试。为了使我们的单元测试发挥作用,我们必须提供每个测试做什么(即,它具体测试什么)以及结果的文档。单元测试和可用工具(集成到我们的持续集成环境中)的优点在于,此过程以一种简化的方式实现了可追溯性和测试条件的再现,这对于我们的 510k 至关重要!
为了实现这一切,我们需要一个能够进行应用程序启动、模拟、模拟对象、模拟接口和临时数据持久化的测试框架。这一切听起来比实际情况要复杂得多,所以请不要担心:好处远大于成本。
持续集成中的即时反馈:开发人员信心
我们经常将测试视为仅在软件开发期间的特定时间进行的活动。最糟糕的情况是,软件测试在开发完成后进行(这时才知道开发远未完成)。在其他更加积极的环境中,它可能在每次迭代结束时进行。我们可以做得更好!如何让复杂的单元测试在每次代码更改时持续进行验证?在每次代码更改时执行完整的回归测试是可能的。听起来像是很大的开销,但事实并非如此。对项目来说,真正的成本不是对复杂的功能单元测试的忽视;危险在于我们把测试推迟到太晚,无法应对在某个预定的测试阶段发现的关键问题。
消灭一个项目最有效的方法是将它组织起来,使测试成为一个对项目成功至关重要的活动,以至于我们不允许测试做它应该做的事情:在上线之前发现缺陷。
在最基本的层面上,持续集成构建环境只做一件事:它运行我们告诉它运行的任何脚本。为此,重要的是 CI 构建执行单元测试,并且任何单个单元测试的失败都被视为持续集成构建的失败。Hudson 或 Jenkins-CI 等工具的强大之处在于,我们可以告诉它运行任何我们想要的东西,记录结果,保留构建工件,运行第三方评估工具并报告结果。通过集成我们的软件版本控制系统(例如,Subversion、Git、Mercurial、CVS 等),我们知道与特定构建相关的更改集。它可以配置为以我们想要的任何间隔生成构建(每晚、每小时、每次代码提交时等)。当测试失败时,我们立即知道哪些更改集参与了。
就我个人而言,每次我进行任何重要的代码提交时,我都会做的第一件事就是检查 CI 构建是否成功。如果我破坏了构建,我就会着手解决问题(如果我不能快速解决问题,我会撤回我的更改集,以便 CI 构建继续运行,直到我修复了问题)。轻松重构
作为开发人员,重构可能是一件很可怕的事情。重构可能是引入严重缺陷的最有效方法,同时又做了一些看似无害的事情。但是,通过彻底的单元测试在每次提交的软件更改集中执行完整的回归测试,开发人员可以确信他们简单的代码更改不会引入缺陷。我们运行持续集成构建来运行我们的测试,原因有很多,其中最重要的原因之一是提醒开发人员他们的更改可能破坏了构建。
作为开发人员,我努力避免破坏持续集成构建。然而,当我确实破坏它时,我很高兴知道导致问题的原因立即被发现了!当一个缺陷的发现直到开发阶段结束才被发现时,修复缺陷的成本会变得高得多!每次代码更改都进行回归测试
我说的“重复”与可重复不同。重复测试的基本好处是,测试可以通过自动化执行的次数比人工测试人员多得多。有时,即使没有相关的代码更改,而且让我们感到惊讶的是,我们会看到一个测试突然失败,而它之前多次成功。发生了什么?
最难修复(更不用说找到)的软件缺陷是那些不一致发生的缺陷。数据库锁定问题、内存问题、死锁错误、内存泄漏和竞争条件会导致此类缺陷。这些缺陷很严重,但如果我们从未检测到它们,我们如何修复它们?
如前所述,必须进行超出我们传统意义上认为的“单元测试”的单元测试,并进一步进行几步,自动化功能测试)。这是一个团队成员经常(错误地)认为没有足够的时间来处理单元测试创建的领域。但是,在适当的框架下,单元测试的创建并不需要让人感到不堪重负。
另一个偶尔出现的问题与软件版本控制系统的误用有关。许多开发人员知道,由于一个开发人员踩了另一个开发人员的修改而导致的意外代码更改会带来的挫折感。虽然在正确使用的版本控制环境中,这是一个罕见的问题,但它确实仍然会发生,并且单元测试可以在构建时快速揭示此类问题。并发测试
并发测试很棘手,并且在并发测试中,功能单元测试的重复和快速性质可以在人工测试人员无法做到的地方发挥作用。我个人目睹过很多次 CI 构建突然失败,没有明显的原因。没有与特定故障点相关的代码提交,但曾经成功的单元测试突然失败了?为什么?
这可能会发生(并且确实会发生),因为并发问题本质上是随机的。有时它们发生的可能性很小,以至于我们在正常的测试过程中从未见过它们。但是,当持续集成环境每天运行并发测试数十次时,我们会增加发现隐藏和威胁性问题的可能性。此外,单元测试可以模拟多个并发用户和进程,即使是一支人工测试人员队伍也无法做到。可重复和可追溯的测试结果
这是使我们的单元测试符合我们在质量系统中制定的标准的关键,以便我们可以将它们用作我们提交的一部分(参见以下关于受监管环境需求的部分)。如果我们要付出努力,并且我们已经知道单元测试会导致软件质量的提高,那么我们为什么不想包含这些测试结果呢?
我们的持续集成服务器可以而且应该用来存储我们的单元测试结果,与它执行的每次构建并排存储。
当然,这不仅仅是 FDA 监管环境中的一个优势。在任何软件项目中,都很难重新创建发现缺陷的条件。通过 CI 构建在已知环境下使用已知文件集(CI 构建工具从版本控制系统中提取)执行我们的构建和测试脚本,可以在精确和特定条件下执行测试。
上面列出的功能单元测试的许多优点只有在单元测试与设计和开发一起编写时才能获得(测试驱动方法除外)。开发团队必须在设计和活动进行时开发和观察测试结果。这对质量保证团队也有好处,正如 Dean Leffingwell 指出:全面的单元测试策略可以防止 QA 和测试人员将大部分时间花在寻找和报告代码级错误上,并允许团队将重点转移到更高级别的系统测试挑战上。事实上,对于许多敏捷团队来说,添加全面的单元测试策略是他们向真正的敏捷过渡的关键转折点——并且是确定整体系统质量的“最划算的投资”。[7] 此外,功能单元测试的主要优势可能是为开发团队提供的实时反馈。一位作者将每次软件更改后执行的单元测试称为“提交测试”。[8]。针对每次签入运行的提交测试为我们提供了有关最新构建中问题以及应用程序中小型错误的及时反馈。[8] - Jez Humble,David Farley,持续集成项目单元测试,它应该提供大量的覆盖率(至少 80%),为团队提供内置的软件更改提交验收标准。如果开发人员因代码更改导致 CI 构建失败,那么立即就知道相关的更改不符合最低接受标准,需要紧急关注。
Humble 和 Farley 继续说道,关键的是,开发团队必须立即响应在正常开发过程中发生的已接受测试的故障。他们必须判断故障是由于引入了回归错误、对应用程序行为的故意更改,还是测试本身的问题。然后,他们必须采取适当的措施,使自动化的验收测试套件再次通过。[8] - Jez Humble、David Farley,《持续集成:规管环境对 21 CFR Part 820(第 C 部分 - 设计控制)的要求》: (f) 设计验证。每个制造商应建立和维护验证设备设计的程序。设计验证应确认设计输出满足设计输入要求。设计验证的结果,包括设计、方法、日期和执行验证人员的识别,应记录在 DHF 中。[5]
简而言之,我们的功能单元测试必须成为 DHF 的一部分,我们必须记录每个测试,每个测试结果(成功或失败),并将测试和结果与特定的软件版本联系起来。在一个持续集成环境中,构建和构建结果(包括测试结果)存储在服务器上,并从我们的 DHF 中进行标记和链接,这使得此过程变得非常容易。事实上,在手动执行和记录测试结果时,有时是一项乏味的任务,现在变得相当方便。
设计验证也是如此:(g) 设计验证。每个制造商应建立和维护验证设备设计的程序。设计验证应在定义的操作条件下,对初始生产单位、批次或批次,或其等效物进行。设计验证应确保设备符合定义的用户需求和预期用途,并且应包括在实际或模拟使用条件下对生产单位进行测试。设计验证应包括软件验证和风险分析(如适用)。设计验证的结果,包括设计、方法、日期和执行验证人员的识别,应记录在 DHF 中。[5] 由于我们的 CI 环境在给定时间点打包构建和测试条件,我们可以轻松地满足 21 CFR 820 第 C 部分第 820.30 条 (f) 和 (g) 中规定的要求。我们只需让我们的 CI 环境做它最擅长的事情,而人类测试员可能需要花费数小时才能准确地完成的事情。记录方法
如上所述,所有这些测试确实对创建良好的软件非常有帮助。但是,如果没有在我们的 FDA 监管环境中明智地使用这些测试,它们在任何可审计能力方面都没有用。我们有必要记录我们在标准操作程序和工作说明中使用和记录单元测试的方法,并且以与记录任何手动验证和确认测试活动相同的方式记录此方法。
为此,有必要使我们的单元测试及其输出成为我们设计历史文件 (DHF) 的一部分。每个测试都必须可追溯,这意味着单元测试被赋予唯一的标识符。这些唯一标识符可以使用我们按逻辑单元(例如,按功能区域)组织测试并将测试按顺序标记的方法轻松分配。标记和追溯测试
我过去使用的一种方法是分配一些高级数字标识符和用于特定测试的二级子标识符。例如,我们可能具有以下功能区域:用户会话、审计日志、数据输入、数据输出和 Web 用户界面测试(这些是功能区域的通用示例)。对于这些功能区域,我将使用测试命名注释,使用以下高级标识符标记每个测试:1000:用户会话测试 2000:审计日志测试 3000:数据输入测试 4000:数据输出测试 5000:Web 用户界面测试 在每个测试中,然后有必要更进一步,将一些顺序标识符应用于每个测试。例如,用户测试包可能包括针对用户登录、用户注销、会话过期和多用户登录并发测试等功能需求的测试。
在这种情况下,我们将标记测试如下:1000_010:用户登录 1000_020:用户注销 1000_030:会话过期 1000_040:多个并发用户登录 使用 TestNG 语法以及适当的 Javadoc 注释,很容易标记和描述测试,以便在我们的 DHF 中包含测试非常简单。 /** * 测试基本用户登录和使用有效用户的会话创建。 * * @throws Exception */@Test(dependsOnMethods = {"testActivePatientIntegrationDisabled"}, groups = {"TS0005_AUTO_VE1023"})public void testActivePatientIntegrationEnabled() throws Exception { Fixture myApp new Fixture(); UserSession mySession = fixture.login(“test_user”, “test_password”); assertNotNull(mySession); asertTrue(mySession.active());} 我们为这些测试选择的任何编号都可以,只要我们在项目级别的文档(例如验证计划或主测试计划)中记录我们对测试标记的方法即可。此类决策留给为 FDA 监管项目设计和应用质量系统的人员。正如我们大多数人现在所知,FDA 并没有告诉我们如何去做,而是告诉我们必须创建一个良好的质量系统,通过设计来追溯或要求,将历史记录纳入我们的 DHF,并且能够重现构建和测试条件。
如果我把这一切听起来太容易了,那是因为我认为它很容易。我们经常把 cGMP 指南看作是对生产力的巨大阻碍。但我们可以控制使事情变得尽可能高效。
可追溯性矩阵
[edit | edit source]使单元测试在可审计的方式下可使用的一个关键因素是将它们纳入可追溯性矩阵。与任何测试一样,要求、设计元素和危害必须通过使用可追溯性矩阵相互追溯。项目团队必须记录要求通过规范和测试的可追溯性,以确保所有要求都已测试并正确实现(产品要求可追溯性矩阵)。[9]Thomas H. Farris,《安全可靠的软件》 随着每个自动化测试的标记,我们可以使用内置的 JUnit 或 TestNG 功能(以及 XSLT,如果我们愿意),创建与构建编号和变更集相关联的输出,并在我们的跟踪矩阵中进行追溯。我们的测试的输出(这些测试在每次持续集成构建期间运行)可能如下所示
TEST NAME STATUSTS0005_AUTO_VE1022 PASSTS0005_AUTO_VE1023 PASS TS0005_AUTO_VE1024 FAILTS0005_AUTO_VE1025 SKIP…
当然,我们希望所有自动化测试都通过,但当它们失败时,我们需要记录它。我认为将所有测试结果放在 DHF 中没有必要。相反,DHF 可以指向持续集成构建服务器,在那里,自动化测试结果与每个构建捆绑在一起。最后,在冲刺或迭代结束时,最终锁定构建的适当测试结果被捕获到 DHF 中并进行适当的追溯(根据标准操作程序)。
我们的 SOP 和工作说明将要求我们证明测试和测试结果的可追溯性,无论是手动测试还是自动化单元测试。正如我们一直在做的手动测试一样,测试必须追溯到软件需求、设计规范、危害和风险。目标只是证明我们已经测试了我们设计和实现的内容,对于自动化测试,这很容易实现!我们是否仍然需要手动测试?
是的!绝对的!手动测试仍然(并将永远)必不可少的原因有很多:安装确认和环境测试。手动测试和自动化测试都是有效的,并且都有价值,两者都不应被视为对方的替代品。
我记得小时候上空手道课。有一天,我从课上回家,非常自豪,因为我已经学会了如何格挡拳击。“用拳头打我吧,”我对我的朋友说。
按照我的要求,他打了我胸部,我未能格挡拳头。这个拳头不是按照我预期的(我们在空手道课上练习的方式)打出来的。
“不,不,不!你打我错了!”我只知道如何格挡一种拳头,当以不同的方式被打时,我的格挡就不起作用了。对我来说,这堂空手道课突出了异常和错误之间的区别。自动化测试可以很好地提供错误测试覆盖率。但是,当遇到意想不到的东西时,它们本身并不能提供创造力来找到问题。
我们,开发人员和测试人员,需要想出创造性的方法来攻击我们的系统。这就是手动测试允许一定程度的“创造性”攻击,而这些攻击在单元测试开发期间可能不会被考虑。手动测试还能更深入地了解可用性和用户交互问题。
也许更重要的是,手动测试可以提供有关一般应用程序可用性和用户交互的反馈。
为此,在手动测试期间发现的缺陷应导致自动化测试。
其他注意事项
测试夹具
[edit | edit source]模拟对象
[edit | edit source]避免使用单例设计模式!
[edit | edit source]内存数据库
[edit | edit source]内存 Servlet 容器
[edit | edit source]参考资料
[edit | edit source][1] 设备建议:法规和指南,软件验证指南,http://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance
[2] 安全可靠的软件 - 为软件医疗设备组织创建高效有效的质量体系,作者 Thomas H. Farris。ASQ 质量出版社,威斯康星州密尔沃基,2006 年,图 4.9,“软件测试类型”,第 120 页
[3] 安全可靠的软件 - 为软件医疗设备组织创建高效有效的质量体系,作者 Thomas H. Farris。ASQ 质量出版社,威斯康星州密尔沃基,2006 年,第 118 页
[4] 安全可靠的软件 - 为软件医疗设备组织创建高效有效的质量体系,作者 Thomas H. Farris。ASQ 质量出版社,威斯康星州密尔沃基,2006 年,图 4.9,“软件测试类型”,第 118 页
[5] CFR - 美国联邦法规第 21 章。子部分 C - 设计控制,第 820.30 节 设计控制
[6] 敏捷软件需求,作者 Dean Leffingwell。Addison-Wesley。版权所有 © 2011,Pearson Education, Inc.,马萨诸塞州波士顿,第 61 页
[7] 敏捷软件需求,作者 Dean Leffingwell。Addison-Wesley。版权所有 © 2011,Pearson Education, Inc.,马萨诸塞州波士顿,第 196 页
[8] 持续交付,作者 Jez Humble,David Farley。Addison-Wesley,版权所有 © 2011,Pearson Education, Inc.,马萨诸塞州波士顿,第 124 页
[9] 安全可靠的软件 - 为软件医疗设备组织创建高效有效的质量体系,作者 Thomas H. Farris。ASQ 质量出版社,威斯康星州密尔沃基,2006 年,图 4.9,“软件测试类型”,第 123 页