一篇文章带你学会软件测试基础

Source

一、软件测试基础

本章将阐述一些软件测试的基本观点,它们是全书的基石。虽然它们并没有得到所有人的认可,但是我认为它们反映了软件测试的基本事实。测试人员需要仔细分析它们,并用于实践,才能有效地实施软件测试。

1.1 软件的复杂度已经超越了人的理解能力

必应词典1是我常用的一款桌面软件(如图1-1所示),它将用户输入的英文单词发送给必应服务器,然后接受服务器所返回的中文解释,最后将中译显示在界面上。

1http://dict.bing.msn.cn

{%}

图 1-1 必应词典

必应词典是一个.NET程序2。它会启动一个专属线程来接受服务器的返回结果,其托管代码(managed code)的调用栈类如代码清单1-1所示。

2讨论的是必应词典1.7版。自2.0版开始,必应词典的主程序(BingDict.exe)不再是.NET程序,而是C/C++程序。

代码清单 1-1 必应词典的调用栈

System.Net.UnsafeNclNativeMethods+OSSOCK.recv(IntPtr, Byte*, Int32, System.Net.Sockets.SocketFlags)
System.Net.Sockets.Socket.Receive(Byte[], Int32, Int32, System.Net.Sockets.SocketFlags, System.Net.Sockets.SocketError ByRef)
System.Net.Sockets.Socket.Receive(Byte[], Int32, Int32, System.Net.Sockets.SocketFlags)
System.Net.Sockets.NetworkStream.Read(Byte[], Int32, Int32)
System.Net.PooledStream.Read(Byte[], Int32, Int32)
System.Net.Connection.SyncRead(System.Net.HttpWebRequest, Boolean, Boolean)
System.Net.Connection.PollAndRead(System.Net.HttpWebRequest, Boolean)
System.Net.ConnectStream.PollAndRead(Boolean)
System.Net.HttpWebRequest.EndWriteHeaders(Boolean)
System.Net.HttpWebRequest.WriteHeadersCallback(System.Net.WebExceptionStatus, System.Net.ConnectStream, Boolean)
System.Net.ConnectStream.WriteHeaders(Boolean)
System.Net.HttpWebRequest.EndSubmitRequest()
System.Net.HttpWebRequest.CheckDeferredCallDone(System.Net.ConnectStream)
System.Net.HttpWebRequest.GetResponse()
System.Web.Services.Protocols.WebClientProtocol.GetWebResponse (System.Net.WebRequest)
System.Web.Services.Protocols.HttpWebClientProtocol.GetWebResponse (System.Net.WebRequest)
System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(System.String, System.Object[])
Engkoo.Ehc.Core.EhcAPI.EhcDataServiceV1.GetEditModeResult(System.String, System.String, System.String, System.String, Int32, Boolean)
Engkoo.Ehc.Core.OnlineDictionaryV2.ProcessEditRequest(DataRequest)
Engkoo.Ehc.Core.OnlineDictionaryV2.AsyncProc()
System.Threading.ThreadHelper.ThreadStart_Context(System.Object)
System.Threading.ExecutionContext.runTryCode(System.Object)
[HelperMethodFrame_PROTECTOBJ: 0da8f3f8]
    System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup
    (TryCode, CleanupCode, System.Object)
System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
System.Threading.ThreadHelper.ThreadStart()

复制代码

该调用栈涉及26个函数,使用了一些典型的开发技术,具体如下所示。

  • Socket通信(System.Net.Socket

  • HTTP通信(System.Net.HttpWebRequest

  • SOAP通信协议(System.Web.Services.Protocols.SoapHttpClientProtocol

  • 异步调用(Engkoo.Ehc.Core.OnlineDictionaryV2.AsyncProc

  • 线程(System.Threading

随着开发技术的发展,现在开发人员不必理解这些技术细节,只要调用程序库,就可以完成客户端(必应词典)与服务端(必应服务器)的通信。相比于底层技术,这些程序库提供了更高的抽象层次。然而,软件专家JoleSpolsky提出了经验法则“抽象漏洞定律”:“所有非平凡的抽象,在某种程度上,都存在漏洞。”[Spolsky02]他敏锐地指出,虽然高抽象层次的语言、程序库和框架等技术提高了开发者的生产力,使他们不必总是关心技术细节,然而总是存在一些情况,需要开发者深入底层,去研究那些为了提高生产率而省略的细节问题。高抽象级别的技术虽然可以减少编写代码的时间,但是并不能减少开发者学习整个技术栈的时间。只停留在高抽象层次的开发者将难以解决复杂的实际问题,且有可能引入更多的设计错误。

对于必应词典,与服务器通信是相对简单的任务,它使用了一些常见的重要技术,开发者完全理解这些技术需要付出很大的努力。然而,必应词典所涉及的技术细节远不止于此,它拥有40多个线程,加载170多个动态链接库,拥有1000多个Windows句柄。毫不夸张地说,几乎没有人可以完全理解它使用的所有技术,也无法掌握它任意时刻的状态。

不过,软件技术只是软件复杂性的一部分。成功的软件必须帮助用户解决实际问题,使他们获得成功。因此,项目团队需要研究用户情景和领域知识,然后创造性地提出解决方案,并选择合适的技术来实现。在这个研究、创造、应用的过程中,项目团队会面临更多的挑战和困难,而能否处理领域复杂度和设计复杂度是决定产品成败的关键。例如,对于必应词典而言,发送请求并接受响应是相对容易的,困难的是为一个单词提供准确的定义、例句、搭配、同义词等信息,更困难的是为一个句子提供恰当的翻译,并流畅地将它朗读出来。这些困难的任务恰恰是用户选择词典软件的重要参考。

由以上讨论不难看出,软件复杂度包含技术、领域、设计等多个方面。随着软件行业的快速发展,软件复杂度已经超越了个人的理解能力。那么,从测试的角度来说,测试人员应该如何应对这一情况呢?本书所介绍的大部分实践都是为了解决该问题的。在这里,我简单陈述一些基本态度和方法。

  • 对于复杂的软件,任何人都不可能掌握全部的信息。如果测试人员对软件的理解存在许多偏差,他的测试策略一定会包含错误。为了更好地理解软件,他需要与产品经理、程序员、领域专家、测试同事等协作,还需要研究项目文档、技术资料、领域专著等文献,并通过实际测试去获得第一手知识。测试人员不应该依赖单一的或局限的信息源,他应该从各种渠道获得信息,多角度地研究软件。

  • 由于软件如此复杂,大多数测试人员在项目之初都不甚了解软件,其拟定的初始测试方案或多或少都存在错漏,有些甚至存在严重错误。注重实效的测试人员,会承认测试方案是不完备的,并迭代地实施测试,通过持续地评估和反思来逐步增强测试方案。

  • 对于现实世界的软件,穷举测试是不可行的。任何实用的测试技术都是基于一定策略的采样,即从无限多的测试输入中选择一个数量有限的子集,通过运行这批测试来评估软件的整体情况。考虑到软件的复杂度,任何一种测试技术都存在不能正确评估软件的风险。而且,在测试之初,测试人员也很难预料究竟哪些测试技术才适合当前项目。为此,测试人员应该积累多种测试技术,综合运用它们,并随着项目发展积极调整测试策略,才能避免重大测试遗漏。

  • 人们处理复杂问题的常用策略是“分而治之”。在软件领域,建立合理的抽象模型是应对软件复杂性的常见方式。在测试领域,测试人员可以建立产品的模型来帮助测试。在建模过程中,他们以当前测试目标为指导,省略无关的产品细节,突出重要的产品元素。面对简化后的产品模型,可以更有针对性地实施测试设计。

  • 在大规模软件中,对于少量代码的变更都不可以掉以轻心,因为修改者、代码评审者、测试者都很难预料这段代码被调用的语境和执行后的影响。例如,程序员要修改网页浏览器(如IE、Chrome、Firefox等)排版引擎的一个排版缺陷,通常需要运行大规模的回归测试。因为全世界的网页千差万别,很难预测这段代码会排版何种网页,也很难预测这段代码是否会破坏某些精心排版的页面。只有运行大规模测试,才能保证代码变更的正确性。

  • 单凭人的脑力已经难以应对软件的复杂度了,测试人员需要考虑利用自动化测试开发强力的测试策略。

1.2 软件测试是获取信息的技术调查

不同的人有不同的背景、不同的目标、不同的任务,对软件测试也会给出不同的定义,因此不存在放之四海皆准的软件测试定义。不过,对于软件测试工程师而言,我认为有3个软件测试的定义很有启发性。

定义1:测试是为了发现错误而执行程序的过程。

该定义是Glenford J. Myers在1979年提出的,历经30多年的考验,至今仍被广泛引用和讨论。这是一句简短而有力的论述,它紧扣测试的基本活动——执行程序和寻找错误,符合大多数测试人员的工作内容,因此更容易获得广泛认可。此外,它揭示了软件测试的根本原因,即某些重要的东西可能出错,软件测试就是要发现这些错误。

它给测试人员的启示是,测试人员应该始终质疑并挑战软件。有些测试任务的字面内容类似于“检查本次代码变更是正确的”,好像要求测试人员去验证软件行为是正确的。对于此类任务,测试人员应该假定代码变更引入了未知的错误,然后想尽办法去攻击软件。即便测试人员最后的结论是“我没有发现错误”,攻击性的测试过程也使这个结论更有可信度。

定义2:测试是一个获取信息的过程,用来降低决策风险。

Genald M. Weinberg认为人不是完美的思考者,面对复杂的情况会作出许多错误的决定。在采购、选择、使用、开发软件的过程中,人们需要作出许多决定。不完美的思考导致有风险的决策,可能造成巨大的损失。他指出测试可以获得更多的信息,使人们更周全地思考,从而降低错误决策的风险。

该定义给了我(一个测试人员)一系列启示。

  • 测试是服务性的工作。通过测试,我向整个团队提供关于产品质量和项目环境的信息,帮助他们作出决定。

  • 低质量的信息不但无助于决策,还可能浪费团队的时间,甚至作出错误的决策。因此,我应该总是提供高质量的信息。这通常体现为及时地交流、周密地测试、仔细地报告。

  • 团队的决定可能与我的期望不一致,要去理解导致该决定的其他信息。例如,我提交了一个严重的缺陷,团队领导却决定不予修复。此时,应该提供更多的信息,据理力争,同时要去理解不予修复背后的原因,从而更好地理解软件和项目。

  • 为了更全面地提供信息,除了运行软件,我还需要更多的获取信息的方法。常见的手段包括与程序员交谈、研究项目资料、学习领域知识、阅读源代码、在调试器中观察软件等。

  • 除了软件缺陷,测试还可以提供关于项目环境的信息。例如,在项目过程中,产品安装总是遇到许多问题。在报告缺陷的同时,我还可以向团队领导反馈:“开发小组是否在产品安装上投入了足够多的资源?因为安装是任何测试的先决条件,安装的错误将阻碍或延缓测试,测试小组建议将它视作第一等的功能需求。”

定义3:软件测试是一种技术调查,其目的是向关系人提供有关产品(软件、系统或服务)质量的实验信息 [Kaner06]。

该定义是Cem Kaner教授提出的,他也认为测试是一种服务。服务的客户是项目关系人,服务的内容是提供产品质量的实验信息。质量就是对某个(某些)人而言的价值 [Gause89]。不同的人对于质量有不同的评判标准,对于信息有不同的需求。常见的项目关系人包括客户(购买产品的人)、用户(使用产品的人)、程序员、产品经理、运维人员、市场营销人员、管理人员等,他们对信息的需求是测试人员主要的工作内容。请看如下案例。

  • 测试经理对测试人员说:“你刚提交了一个严重的缺陷,做得好!但是,现在临近发布,修复它的风险很高。你能不能调查一下这个缺陷在哪些情况下出现,会影响哪些用户,导致的最坏结果是什么?我们会根据这些信息来决定是否修复和修复策略。”

  • 程序员对测试人员说:“我完成了一个补丁的开发,下周将部署到产品环境中。你能否在预发布环境中部署它,并跑一些系统测试?它只是缺陷修复,没有引入新功能,应该不会破坏当前系统的运行。”

  • 产品经理对测试人员说:“我们在当前版本中启用了新的图形渲染引擎,能够明显提高画质,但是试用者反映这个版本的性能不如上一版。你能不能做一些实验,看看当前版本在哪些场景中比较慢,慢多少?”

  • 运维人员对测试人员说:“一个用户在产品论坛上发帖说,软件在启动时会弹出一个报错对话框,跟帖的几个人也说遇到过相同的情况。我从没有见过该问题,所以不知如何解决。你能不能看一下这个帖子,试着在测试环境中复现?这个缺陷在下一版已经修复了吗?”

以上案例都表明,除了发现缺陷外,测试人员还通过多种方式向不同的关系人提供信息:向管理人员提供调查报告,向程序员提供代码的质量反馈,向产品经理提供技术支持,向运维人员提供已知缺陷的信息,在邮件组中回答用户提问等。为了更有效地测试,测试人员需要与测试经理讨论,设定测试服务的优先级,为关系人提供满足其要求的高质量的信息。例如,提供给测试经理的信息要面向项目风险,提供给程序员的信息要包含更多的技术细节,提供给产品经理的信息要侧重于用户体验和产品价值,提供给运维人员的信息要建议可能的解决方案。

此外,Cem Kaner还强调软件测试的主要工作是技术调查,即以职业的态度、专业的技能对产品的未知领域进行探索。与地质勘查、罪案侦查、事故调查等调查活动相似,测试具有系统性、客观性、探索性和机动性。

  • 测试应该系统地调查被测试对象。测试人员需要对产品进行周密的分析与检查。地质勘查结束后,勘查人员会给出详细的地质图,以描绘该地区的全貌。测试结束后,测试人员也应该全面掌握被测试对象的情况。

  • 测试所提供的信息应该来自科学实验和中立观察。在真实和客观的基础上,包含合理的推测和建议。

  • 刑侦人员会利用多种方法,从各个信息源收集情报,因为很难预测何处会有重大发现。当突破性情报浮现时,他们会转移工作重点,顺着新线索紧追不舍。测试人员也需要综合运用多种技术和工具,去探究新的信息,并根据新发现及时调整测试方向。

  • 优秀的潜水员既能够通过浮潜去游历宽阔的水域,也可以利用水肺潜水去探索深海水域。测试人需要交替使用广度调查和深度调查,从而更有效地提供信息。广度调查有助于发现高风险的区域,深度调查能够提供详细的信息,以支持项目关系人作出正确的决定。

1.3 测试是迭代过程

测试是一个贯穿项目周期的调查过程。为了阐述技术细节,许多测试文献只讨论了该过程的一小部分。因此,它们无意中将测试描绘为一个线性过程:测试人员获得测试任务,对被测软件进行分析,制订测试计划,确定测试用例,然后……没有“然后”了,因为大多数文献只写完第一轮测试设计便结束了。

但是测试并不是一个线性过程。

1. 第一个测试周期

 1.1 第1步:从显而易见的简单测试开始

 1.2 第2步:记录还需要测试什么

 1.3 第3步:检查有效用例并观察发生了什么

 1.4 第4步:做一些“快速”测试

 1.5 第5步:总结对程序和问题的认识

2. 第二个测试周期

 2.1 第1步:在进行任何测试之前应该仔细评审对问题报告的反馈,以确定哪些部分值得深入测试

 2.2 第2步:评审对不予修复的问题的意见,它们可能建议进一步的测试

 2.3 第3步:找出上次的测试笔记,加入新笔记,并开始测试

3. 后续测试周期中可能会发生的事情

该目录列举了两个测试周期中测试人员的典型活动。测试周期是一个相对于测试人员而言的概念,它包含获得可测试的新版本、测试、报告问题(即bug)、反思总结等活动。无论项目采用何种开发模型,测试人员总是一个版本接一个版本地测试,其测试活动总是迭代向前的:测试版本1→提交缺陷→修复缺陷→测试版本2→提交新缺陷→修复新缺陷→测试版本3→……

即便在测试周期内部,测试活动也是迭代的。以“第一个测试周期”为例,测试人员不熟悉软件,所以第1步是运行一些简单的测试,来了解软件的行为。在第2步和第3步,测试人员根据第1步获得的信息设计了更多的测试,并记录在案。他使用了一些经典的测试设计方法,如边界值分析和等价类划分。在第4步,他使用了一些启发式测试方法对软件进行快速攻击。在第5步,他对被测软件和测试策略进行了反思,以挖掘软件的风险和测试的不足。可见,整个测试过程是螺旋向上的:初始的测试提供了正式测试设计所需的信息;正式的测试用例较系统地检查了系统;当正式测试揭示出软件风险时,快速测试机动地探索相关领域,以确定是否存在严重的问题,并为后续的正式测试提供信息。

后续的测试周期会利用先前测试所产生的信息。以“第二个测试周期”为例,测试人员分析对问题报告的反馈,了解团队中其他人对软件行为的看法。利用这些新信息,他可以确定测试的重点。他还会参考第一个周期的测试笔记,但是不会照本宣科地重新执行一遍,而是利用新信息设计新的测试。从这个角度来看,测试类似于移动的军舰向另一艘移动的军舰开火。开火的军舰在移动是因为测试人员对被测软件的认识在不断地增强,他知道哪些测试不太容易找到错误,哪些测试更可能找到问题。目标军舰在移动是因为程序员在持续提交新功能和修复缺陷,导致软件也在不停地变化。因此,测试策略也需要保持动态变化,使得炮口一直瞄准飞驰的目标。

迭代的最大优点是能够快速获得测试设计的反馈,从而逐步完善测试设计。程序员已经认识到软件开发过程会引入大量的错误,应该利用快速反馈来发现设计中的错误。因此,测试驱动开发、结对编程、持续集成、自动化测试等技术得到了普遍认可和应用。测试设计与软件设计相似,都是高智力的创造性工作,也可能会产生错误的设计。测试人员也需要快速获得测试设计的反馈,有意识地利用迭代可以为此奠定良好的基础。

1.4 测试人员的工作效率取决于他对软件和项目的理解,而不是他掌握的测试技术

我曾经长期测试一个网络应用。当我离开这个项目时,测试经理安排一个测试员工来接替我。他刚刚入职,对被测软件和业务领域都不了解,在工作中遇到了许多困难。在我加入新团队后,我还通过电话数次回答了他的问题。后来,我反思了当时的情况。作为一个测试工程师,我的工作效率明显优于接替我的同事,主要原因包括以下几点。

  • 我理解产品。我知道它的业务目标,了解它通过什么方法去实现目标。因此,我能够快速地制定测试方案。

  • 我理解用户的期望。我知道哪些功能绝对不能出错,需要仔细测试,也知道哪些功能允许一些瑕疵,即便出错,也可以在下一个版本(通常在3个月之后发布)中修复。因此,我能够更好地分配测试时间。

  • 我理解产品的架构。通过阅读产品源代码,我知道哪些模块容易出现哪些缺陷。因此,我可以针对不同的模块采用有针对性的测试策略。

  • 我知晓如何回避浪费时间却没有收益的任务。例如,我曾经尝试用自动化测试用例去测试用户界面,但是发现此类自动化测试很不稳定,需要很高的维护代价,却不能发现错误。于是,我只为Web服务编写自动化测试用例,用手工测试来测试用户界面。

  • 我了解产品元素和项目团队。当出现缺陷时,我知道如何阅读系统日志发掘蛛丝马迹;当我遇到困难时,我知道向哪位程序员或测试人员求助。因此,我可以深入挖掘并快速推进。

  • 我在原先的团队工作了很长的时间,与同事建立了良好的关系。当我提出一些可测试性的建议时,比较容易得到程序员的支持。

从以上几点不难看出,我能够更有效地测试,其主要原因不是我掌握更多的测试技术,而是我更了解软件产品、业务领域和项目环境。通过逐点分析,可以得到如下启示。

  • 产品是一种解决方案,如果没有解决问题,它就是无用的[Kaner12]。测试人员需要了解软件产品和业务领域,才能设计有效的测试。

  • 测试是一种信息服务,要了解服务对象的需求。如果用户不能容忍某些错误,测试人员就需要仔细测试相关功能;如果用户对一些瑕疵并不在意,测试人员就不必在此花费过多的时间。只有了解服务对象的优先级,才能更好地设定测试工作的优先级。

  • 不同的模块采用不同的技术,拥有不同的典型错误。只有了解软件实现,才能设计差异化且有针对性的测试用例。

  • 测试设计可能包含错误,测试人员需要从错误中吸取经验和教训,避免重蹈覆辙。

  • 当测试工作遇到困难时,测试人员需要知道从哪里寻找信息。了解被测产品和测试工具能够提供的信息,了解哪位同事知道更多内幕,会节省时间。

  • “人脉”有时候会极大地提高测试人员的工作效率。测试人员需要与程序员和测试同事保持良好的关系。达成协作关系的关键之一是测试人员能够为同事们提供高质量的信息服务。

  • 在职业生涯中,测试人员总是会遇到新的软件、项目和团队。他应该养成一种好的思维方法和测试风格,以便快速地学习并理解产品和项目。

实施高效的测试需要很多条件。熟练地掌握测试技术是一个很重要的因素,但很少会是决定性的因素。只有充分掌握软件产品和项目环境,测试技术才能找到大放光彩的舞台。

1.5 小结

本文介绍了软件测试的基本价值观。

自学时找不到资料?
b站2021最新软件测试合集https://www.bilibili.com/video/BV1ey4y1W7n6/
没人解答问题?
可以加我的软件测试交流群:1140267353进群暗号csdn小jiang
里面有各种软件测试学习资料和技术大佬讨论。
  • 软件复杂性来自于领域、设计、技术、开发过程等多个方面。为了应对高度复杂且持续变动的软件,测试人员需要从多个来源收集信息,并在项目全程收集对测试设计的反馈。

  • 软件测试的基本目标是发现软件错误,并予以修正。

  • 软件测试是一种信息服务。以技术调查和科学实验的方式实施软件测试,能够提供高质量的服务。

  • 无论测试人员是否意识到,软件测试都是迭代展开的。有意识地利用软件测试的迭代性,可以更好地测试。

  • 高效的软件测试要求测试人员理解软件和项目的方方面面。学习与实践要伴随测试全程。