Swift的世界,如何写好单元测试?

发表于:2017-9-19 14:16

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Maru    来源:51Testing软件测试网采编

  前言
  Unit Test.png
  作为一名无所事事的公司蛀虫,总是想在平静的日子里搞出点事情。于是我发现,公司的网络层作为基础库竟然没有单元测试覆盖,是不是有失软件工程水准呢?于是就有了接下来的故事...
  Why?
  当我们做某件事情的时候,我们常常抱有强烈的目的性,那么单元测试的目的是什么呢?为什么要有单元测试呢?
  遗憾的是,作为一个‘人’,我们无法控制我们想控制的事物按照预想的情况运作下去。即便是那些很厉害很厉害的开发人员,在介绍他的时候也只能说“几乎没有BUG”,而那些肉眼我们无法察觉的BUG就需要我们通过测试来发现并且修正它了。
  What?
  那么说了那么多,到底什么是单元测试呢?我们可以来看一下维基百科上的定义。
  In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.
  在计算机编程中,单元测试是一种软件测试方法,通过该方法测试各个单位的源代码,一个或多个计算机程序模块的组合以及关联的控制数据,使用和操作程序,以确定它们是否正常运行。
  简单的来说,单元测试是使用程序控制的以类或者函数为单元的期望判断。比如,我们需要测试一个计算器中的加法(来自于Apple官方文档):
  - (void)testAddition
  {
  // obtain the app variables for test access
  app                  = [NSApplication sharedApplication];
  calcViewController   = (CalcViewController*)[[NSApplication sharedApplication] delegate];
  calcView             = calcViewController.view;

  // perform two addition tests
  [calcViewController press:[calcView viewWithTag: 6]];  // 6
  [calcViewController press:[calcView viewWithTag:13]];  // +
  [calcViewController press:[calcView viewWithTag: 2]];  // 2
  [calcViewController press:[calcView viewWithTag:12]];  // =
  XCTAssertEqualObjects([calcViewController.displayField stringValue], @"8", @"Part 1 failed.");
  }
  在这个中,我们的测试目的只有一个,那就是在加法的情况之下,进行6+2的运行,并且期望结果为8,如果期望不满足,那么Xcode就会在该断言上失败。这几乎是最简单的一个单元测试了,但是在真实的世界中,我们所碰到的情况比这复杂的多。比如,我们需要测试的方法是异步的,我们所测试的方法互相依赖,我们需要测试一个方法的性能等等,那么如何在真实的复杂情况之下编写出令人满意的良好测试呢?
  命名
  按照Apple官方文档,相信你能很快的新建一个项目的测试Target,当你新建一个.swift文件之后你的心中可能会突然一下颤抖,然后发出宇宙终极的三问:我在哪?我是谁?我在干什么?
  是的,你在公司,你是一个死宅码农,你在写单元测试!
  可是,你却迟迟不能动手写下一行代码,不是因为你不知道想要测试什么功能,你知道你想测试网络层的Get请求是否正常运作,但是你不知道该怎么样给这个测试取一个名字,就好像一个爸爸看到刚出生的baby一样手足无措。
  要不就叫它func testGet()吧!
  然而当你敲下方法名的定义之后,你敏锐的工程师思维开始发挥了作用,如果我的Get方法带参数怎么办?如果不带参正常运行,带参失败了怎么办?也就是说我不止需要一个Get的测试方法,那么我的命名应该如何呢?
  确实,这样的测试不仅没有能够测试到应该覆盖的测试case,同时也不便于维护,他人很难通过方法命名一眼就看出你测试的意图。
  那么良好的测试命名应该是怎么样的呢?
  总的来说,良好的测试命名应该有如下的特点:
  全局测试内的命名统一。
  命名可以清晰的阐明测试意图。
  命名可以清晰的阐明测试期望以及副作用(如果有的话)。
  1.Plan A
  在A方案中,我们单元测试的名称将分为三部分:方法名称(method name)+ 执行测试用例的状态(state under test)+ 预期名为(expected behavior)示例如下:
  /// 这是一个除法的测试,在分母为0的情况之下,我们期望抛出异常
  func divide_ZeroAs2ndParam_ExceptionThrown()
  可以看到,在这样的命名规范之下,他人也可以通过方法名清晰明了的知道该方法在怎样的期望输入或者状态之下会产出什么样的输出或者状态。
  Tips:当我们修改了所测试的方法名字之后,原测试方法就已经偏离了命名规范,所以需要我们手动的修改测试方法。但是这样的工作明显是最无效和重复的。因此也可以这样做:我们将原来的方法名称(method name)更改成了抽象的方法名称,而不是将原来的方法名称一字不落的当做测试方法的前缀。
  2. Plan B
  在B方案中,我们将采用Given-When-Then的方式进行命名组织,该组织方式来源于BDD(Behavior-Driven Development)。具体的命名例子如下:
  /// Given: 当前测试所给予的输入或者初始状态
  /// Action: 当前测试所要进行的操作
  /// Then: 当前测试所期望的输出状态或者输出
  func Given_StateUnderTest_When_ActionUnderTest_Then_ExpectedOutcomes()
  我们可以看到,在Given-When-Then的命名方式之下,我们满足了所有良好测试命名的特点,与此同时似乎还看起来有一些过于“啰嗦”,但是这也并不是什么大问题,毕竟清晰的意图的优先级总比简短的命名优先级更高。
  总的来说,测试的命名并没有刻板的规定,只要满足自身的测试需要,满足公认的测试名称规范就可以。当然还有一些其他的命名方式,但是基本上也都是与上述的两种方法类似或者是变种。最重要的是,我们知道了命名的准则,那么我们也可以制作出属于自己的规范。
  关于断言数的争论
  在我跟同事关于单元测试的讨论中,同事提出单元测试最好只有一个assert,不然当测试不通过的时候无法知道具体fail在哪里。但是,具体在iOS的XCTest中,我们知道当某一个断言无法满足条件的时候,Xcode会直接卡在那个断言之上,并且告诉你不通过的原因,如下图所示:
  断言不满足.png
  但是我也知道,一个单元测试的用例最好只包含一个assert这样的观点也由来已久,那么到底在编写单元测试的用例的时候该不该使用多个断言呢?
  我们先来看看赞成单元测试用例只写一个断言的其他理由:
  如果你在一个测试中包含了不只一个断言,则你的测试目的就不只一个。在这种情况下,测试名称变得奇怪不清晰,测试变得太长,反馈也变得不清晰;你永远无法知道哪个断言通过了,哪个断言失败了。假如你依次有三个断言。如果第一个断言失败了,则后面两个永远都不会检查。如果你修改了一些生产代码,那么当代码变化时,后面两个断言就无法发挥作用了。在这种情况下,你就会错误地认为自己的代码有安全保障和回归测试。 ---编写良好的单元测试
  其实,我确实同意上述的某些观点的,比如测试的目的应当只有一个,
  但是当你只有一个测试目的的时候就代表我们只能有一个断言么?我想这个推论应当是错误的。
  我们可以在StackOverFlow里看到相关的讨论,其中第二个回答我深以为然,比如我们要测试所得到数值是否在一个数值区间内,我们的单元测试代码可能是这样的:
  public void ValueIsInRange()
  {
  int value = GetValueToTest();
  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
  }
  在这里我们所要测试的确确实实是一个单独的目的,即“该数值是否在某个区间内”,但是很显然我们需要两个断言来分别判断数值的上界和下界。当然我们也可以通过isInRange之类的方便来将两个断言合并成一个,但是这样真的是一个好的测试用例么?当用例的失败的时候,我们只能知道该数值不在指定的范围内,但是我们甚至都不知道它是超过了上界还是下界。
  综上所述,“一个单元测试最好只有一个断言”并不十分准确,或许我们应当信奉的应该是“一个单元测试应当只有一个逻辑单元,只有一个测试目的”,本着这样的宗旨,写出只有一个断言的测试应该是自然而然的事情,在需要的时候可以使用多个断言。

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号