编写可维护软件的不朽代码随想-10
作者:互联网
自动化开发部署和测试
在之前章节中有一个IsValid方法,检查银行账号是是否符合校验码要求,由于这种方法很容易出现代码错误,都会写一个短小的程序来测试验证此方法。
class Program { [STAThread] static void Main(string[] args) { string acct; do { Console.WriteLine("输入一行账号:"); acct = Console.ReadLine(); Console.WriteLine($"银行账号'{acct}'是" + (Accounts.IsValid(acct) ? "" : "不") + "合法"); } while (!string.IsNullOrEmpty(acct)); } }View Code
这样的程序是一个手动单元测试,因为它只测试一个代码单元IsValid,又因为需要用户手动输入测试用例,所以称为手动单元测试。
这种单元测试有几个缺点:
测试用例需要手动提供,测试程序无法自动执行;
编写测试的开发人员必须专注于执行测试的逻辑,而不是测试本身;
程序并没有展示出对IsValid方法如何运行的期望;
程序无法识别为一个测试程序(Program名称太通用,表示它只不过是一个一次性的实验程序)。
自动化单元测试就是自动运行的,用来测试代码单元本身的代码。NUnit是一个常用的框架。
如何使用本原则:
单元测试:一个代码单元的单一功能,验证代码单元行为是否符合预期,测试执行者是开发人员。
集成测试:至少两个类中的功能、性能或者其他质量特征,验证系统的各个部分是否能够在一起工作, 测试执行者是开发人员。
端到端测试:(与用户或者另一个系统)系统交互,验证系统的行为是否符合预期,测试执行者是开发人员。
回归测试:之前代码单元、类或者系统交互的错误行为,保证BUG不再出现,测试执行者是开发人员。
验收测试:(与用户或者另一个系统)系统交互,确认系统的行为与需求一致,终端用户代表是测试执行者。
不同类型的测试需要不同的自动化框架,单元测试可以使用NUit。端到端测试,需要模仿用户输入并捕获输出的框架,Selenium框架。集成测试可以用SoapUI框架。
NUint测试入门:
如果测试Accounts类的IsValid方法,那么Accounts成为被测试类。用[TestFixture]属性标识一个类时,这个类就是一个测试固件。按照约定,测试类的名称是被测试类名加上Test。测试类必须是公开的类,不需要继承任何其他类。
[TestFixture] public class AccountsTest { [Test] public void TestIsValidNormalCases() { Assert.IsTrue(Accounts.IsValid("123456789")); Assert.IsFalse(Accounts.IsValid("123456788")); } } public class Accounts { public static bool IsValid(string number) { int sum = 0; for (int i = 0; i < number.Length; i++) { sum += (9 - i) * (int)char.GetNumericValue(number[i]); } return sum % 11 == 0; } }View Code
需要引入Nuget包,NUnit包。
在VS中可以直接运行单元测试。
空字符串也不是有效的银行账号,继续增加测试方法:
从图上的返回结果看,这个单元测试返回false,失败的测试让我们意识到IsValid方法存在一个缺陷,如果传空字符串,那么它会返回true,这就不是预期的了,需要更改这个方法,检查银行账号的长度。
测试失败意味着测试本身执行完成,断言失败了。以下这个方法就会抛出一个除以0的异常,甚至永远不会执行断言那句代码。
以下是一些基本原则,来帮助你编写良好的单元测试。
1 正常情况和特殊情况都要进行测试
2 像维护非测试代码一样维护测试代码
3 编写独立的测试,它们的输出应该只反映被测试主体的行为:每个测试应该与其他的测试相互独立。
在单元测试中,应该模拟其他类所需的状态输入,否则,这个测试就不是独立的,会测试多个代码单元。对于IsValid方法,编写测试并不难,因为也没有调用系统中的其他方法,但是对IsValid方法只接受一个字符串作为参数,对于其他情况可能需要stubbing或者mocking的技术。
public interface ISimpleDigitalCamera { Image TakeSnaphot(); void FlashLightOn(); void FlashLightOff(); }
假设某个提醒人们晚上关灯的应用程序,如下:
public const int DAYLIGHT_START = 6; public Image TakePerfectPicture(int currentHour) { Image image; if (currentHour<DAYLIGHT_START) { camera.FlashLightOn(); image = camera.TakeSnaphot(); camera.FlashLightOff(); } else { image = camera.TakeSnaphot(); } return image; }
逻辑简单,但仍需要测试。拍照片的过程需要时自动化并且独立的,数字相机接口的正常实现是不能用的,在一台传统的设备上,正常的实现需要一个用户将相机指向某个感兴趣的东西,然后按动按钮,拍照下来可以是任何图片,所以很难测试拍下来的照片是否是想要的。
解决办法是使用一个专门用于测试的相机接口实现。这个实现是一个假对象,称为测试桩stub。希望这个假的对象能够按照事先定好的方式来运行,编写了一个类似这样的测试stub:
class DigitalCameraStub : ISimpleDigitalCamera { public Image TestImage; public Image TakeSnapshot() { return this.TestImage; } public void FlashLightOn() { } public void FlashLightOff() { } }
现在可以在测试中使用这个stub:
[Test] public void TestDayPicture() { Image image = Image.FromFile("../../../test/resources/van.jpg"); DigitalCameraStub cameraStub = new DigitalCameraStub(); cameraStub.TestImage = image; NoticeDemo.camera = cameraStub; Assert.AreSame(image, new NoticeDemo().TakePerfectPicture(12)); }
本次测试中,创建了一个相机的stub,并且让它返回了一张图片,然后调用TakePerfectPicture(12),并且检查是否返回了正确的图片。
现在假设希望测试TakePerfectPicture在夜间的行为,即希望确保如果调用TakePerfectPicture的参数小于DAYLIGHT_START,会打开闪光灯。实际测试的是TakePerfectPicture方法是否会调用FlashLightOn方法。但是FlashLightOn方法不会返回任何值,并且接口也没有提供任何能够知道闪光灯是否被打开的方法。如何来检查?解决方法是提供一个假的数字相机实现,通过某些机制记录下感兴趣的方法是否被调用过。一个记录调用是否发生的虚假对象称为一个mock对象。mock对象就是i一个具有特定测试行为的stub对象。
class DigitalCameraMock : ISimpleDigitalCamera { public Image TestImage; public int FlashOnCounter = 0; public Image TakeSnapshot() { return this.TestImage; } public void FlashLightOn() { FlashOnCounter++; } public void FlashLightOff() { } }
与stub相比,mock通过一个公共字段保存FlashLightOn方法被调用的次数。以下就是检查FlashLightOn方法是否被调用过:
[Test] public void TestNightPicture() { Image image = Image.FromFile("../../../test/resources/van.jpg"); DigitalCameraMock cameraMock = new DigitalCameraMock(); cameraMock.TestImage = image; NoticeDemo.camera = cameraMock; Assert.AreSame(image, new NoticeDemo().TakePerfectPicture(0)); Assert.AreEqual(1, cameraMock.FlashOnCounter); }
虽然编写了自己的stub和mock对象,不过也产生了大量代码。使用Moq这样的Mocking框架最有效率,Mocking框架利用.Net运行时特性,自动创建普通接口或类的mock对象,还提供了各种方法,用来测试mock对象的某些方法是否被调用,以及记录调用的参数。有些Mocking框架还能够为mock对象指定预定义的行为,让他们能够同时具有stub和mock的特征。
如果以Moq为例,不需要自己编写DigitalCameraMock类,就可以实现与TestNightPicture一样的效果:
[Test] public void TestNightPictureMoq() { Image image = Image.FromFile("../../../test/resources/van.jpg"); var cameraMock = new Mock<ISimpleDigitalCamera>(); cameraMock.Setup(foo => foo.TakeSnaphot()).Returns(image); NoticeDemo.camera = cameraMock.Object; Assert.AreSame(image, new NoticeDemo().TakePerfectPicture(0)); cameraMock.Verify(foo => foo.FlashLightOn(), Times.AtMostOnce()); }
通过Moq的Setup和Returns方法,为其指定了期望的行为。Moq的Verify方法验证FlashLightOn方法是否被调用过。
测量覆盖率确定是否有足够的测试用例
究竟需要多少单元测试,覆盖率是在单元测试中执行的代码行数占代码库总行数的百分比。根据经验,测试用例至少保证80%的覆盖率。
为什么说不是100%,任何代码库都会有一些很琐碎简单的代码片段,比如C#的getter:public string Name {get;}
虽然可以测试这个方法,Assert.AreEqual(myObj.getName(), "John"),但是这种测试更多的是在测试C#编译器和.Net运行时是否正常。但是也不是说都不应该测试getter方法,比如一个表示邮箱地址的类,会有两到三个字符串表示额外的地址信息,经常会出现以下错误:
public string getAddressLine3() { return this.addressLine2; }
只有80%的覆盖率不足以保证高质量的单元测试,只测试一些高层的方法而不是mock底层的方法,也可能达到高覆盖率,但是并不能带来任何质量的提升。因此建议生产代码和测试代码一比一。
常见反对意见
我们仍需要手动测试:手动测试速度慢,成本高,很难按照相同的方法重复执行。都要对系统的技术性验证 ,无法对根本不能正常工作的代码进行手动测试,由于手动测试难以被重复执行,即使只是改动了很少的代码,也不得不对系统重新再全部进行手动测试。
上级不允许我编写单元测试:经理说编写单元测试会降低生产效率。在开发过程中编写单元测试实际上可以提高生产率,它通过将关注从“代码应该做什么”转移到“代码不应该做什么”,从而改进了系统代码,如果你从没想过代码可能如何失败,你就无法保证代码是否可以应对意料之外的情况。没有单元测试的坏处,在于不确定性与重复工作,每当一段代码被修改后,都需要小心地审查以确定正确。
既然现在代码覆盖率本来就低,为什么还要再投入单元测试:当一个非常大型的系统只有很少一些、甚至没有单元测试时,这可能就成为了一个负担。从头开始为已有系统编写单元测试将会是很巨大的投入,需要再一次分析所有的代码单元,只有在确定这会带来相应价值时,才应该在单元测试方面大力投入,尤其是对于关键的重要功能,当我们有理由相信它会出现意外行为时,更应该补充单元测试。除此之外,你应该在每次修改已有代码或者添加新代码时,渐进性地增加单元测试。
SIG评价可测试性
可测试性时ISO中可维护性的5个子特征之一,通过汇总单元复杂度,组件独立性,系统体积,这些系统属性的评级来评估可测试性,这样做的理由是,复杂的代码单元非常难以测试,组件之间缺乏独立性也会增加mock和stub的工作量,大量的生产环境代码也会导致需要更多的测试代码。
标签:10,Image,代码,单元测试,方法,测试,不朽,随想,public 来源: https://www.cnblogs.com/yorkness/p/14785480.html