其他分享
首页 > 其他分享> > 编写可维护软件的不朽代码随想-10

编写可维护软件的不朽代码随想-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