前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >.NET单元测试的艺术-2.核心技术

.NET单元测试的艺术-2.核心技术

作者头像
Edison Zhou
发布于 2018-08-20 09:31:59
发布于 2018-08-20 09:31:59
1.8K00
代码可运行
举报
文章被收录于专栏:EdisonTalkEdisonTalk
运行总次数:0
代码可运行

开篇:上一篇我们学习基本的单元测试基础知识和入门实例。但是,如果我们要测试的方法依赖于一个外部资源,如文件系统数据库、Web服务或者其他难以控制的东西,那又该如何编写测试呢?为了解决这些问题,我们需要创建测试存根伪对象模拟对象。这一篇中我们会开始接触这些核心技术,借助存根破除依赖,使用模拟对象进行交互测试,使用隔离框架支持适应未来和可用性的功能。

一、破除依赖-存根

1.1 为何使用存根?

  当我们要测试的对象依赖另一个你无法控制(或者还未实现)的对象,这个对象可能是Web服务、系统时间、线程调度或者很多其他东西。

  那么重要的问题来了:你的测试代码不能控制这个依赖的对象向你的代码返回什么值,也不能控制它的行为(例如你想摸你一个异常)。

  因此,这种情况下你可以使用存根

1.2 存根简介

  (1)外部依赖项

一个外部依赖项是系统中的一个对象,被测试代码与这个对象发生交互,但你不能控制这个对象。(常见的外部依赖项包括:文件系统、线程、内存以及时间等)

  (2)存根

一个存根(Stub)是对系统中存在的一个依赖项(或者协作者)的可控制的替代物。通过使用存根,你在测试代码时无需直接处理这个依赖项。

1.3 发现项目中的外部依赖

  继续上一篇中的LogAn案例,假设我们的IsValidLogFilename方法会首先读取配置文件,如果配置文件说支持这个扩展名,就返回true:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public bool IsValidLogFileName(string fileName)
    {
        // 读取配置文件
        // 如果配置文件说支持这个扩展名,则返回true
    }

  那么问题来了:一旦测试依赖于文件系统,我们进行的就是集成测试,会带来所有与集成测试相关的问题—运行速度较慢,需要配置,一次测试多个内容等。

  换句话说,尽管代码本身的逻辑是完全正确的,但是这种依赖可能导致测试失败。

1.4 避免项目中的直接依赖

  想要破除直接依赖,可以参考以下两个步骤:

  (1)找到被测试对象使用的外部接口或者API

  (2)把这个接口的底层实现替换成你能控制的东西;

  对于我们的LogAn项目,我们要做到替代实例不会访问文件系统,这样便破除了文件系统的依赖性。因此,我们可以引入一个间接层来避免对文件系统的直接依赖。访问文件系统的代码被隔离在一个FileExtensionManager类中,这个类之后将会被一个存根类替代,如下图所示:

  在上图中,我们引入了存根 ExtensionManagerStub 破除依赖,现在我们得代码不应该知道也不会关心它使用的扩展管理器的内部实现。

1.5 重构代码提高可测试性

  有两类打破依赖的重构方法,二者相互依赖,他们被称为A型和B型重构。

  (1)A型 把具体类抽象成接口或委托;

  下面我们实践抽取接口将底层实现变为可替换的,继续上述的IsValidLogFileName方法。

Step1.我们将和文件系统打交道的代码分离到一个单独的类中,以便将来在代码中替换带对这个类的调用。

  ①使用抽取出的类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public bool IsValidLogFileName(string fileName)
    {
        FileExtensionManager manager = new FileExtensionManager();
        return manager.IsValid(fileName);
    }

  ②定义抽取出的类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class FileExtensionManager : IExtensionManager
    {
        public bool IsValid(string fileName)
        {
            bool result = false;
            // 读取文件

            return result;
        }
    }

Step2.然后我们从一个已知的类FileExtensionManager抽取出一个接口IExtensionManager。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public interface IExtensionManager
    {
        bool IsValid(string fileName);
    }

Step3.创建一个实现IExtensionManager接口的简单存根代码作为可替换的底层实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class AlwaysValidFakeExtensionManager : IExtensionManager
    {
        public bool IsValid(string fileName)
        {
            return true;
        }
    }

  于是,IsValidLogFileName方法就可以进行重构了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public bool IsValidLogFileName(string fileName)
    {
        IExtensionManager manager = new FileExtensionManager();
        return manager.IsValid(fileName);
    }

  但是,这里被测试方法还是对具体类进行直接调用,我们必须想办法让测试方法调用伪对象而不是IExtensionManager的原本实现,于是我们想到了DI(依赖注入),这时就需要B型重构。

  (2)B型 重构代码,从而能够对其注入这种委托和接口的伪实现。

  刚刚我们想到了依赖注入,依赖注入的主要表现形式就是构造函数注入与属性注入,于是这里我们主要来看看构造函数层次与属性层次如何注入一个伪对象。

  ① 通过构造函数注入伪对象

  根据上图所示的流程,我们可以重构LogAnalyzer代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class LogAnalyzer
    {
        private IExtensionManager manager;

        public LogAnalyzer(IExtensionManager manager)
        {
            this.manager = manager;
        }

        public bool IsValidLogFileName(string fileName)
        {
            return manager.IsValid(fileName);
        }
    }

  其次,再添加新的测试代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [TestFixture]
    public class LogAnalyzerTests
    {
        [Test]
        public void IsValidFileName_NameSupportExtension_ReturnsTrue()
        {
            // 准备一个返回true的存根
            FakeExtensionManager myFakeManager = new FakeExtensionManager();
            myFakeManager.WillBeValid = true;
            // 通过构造器注入传入存根
            LogAnalyzer analyzer = new LogAnalyzer(myFakeManager);
            bool result = analyzer.IsValidLogFileName("short.ext");

            Assert.AreEqual(true, result);
        }

        // 定义一个最简单的存根
        internal class FakeExtensionManager : IExtensionManager
        {
            public bool WillBeValid = false;
            public bool IsValid(string fileName)
            {
                return WillBeValid;
            }
        }
    }    

Note:这里将伪存根类和测试代码放在一个文件里,因为目前这个伪对象只在这个测试类内部使用。它比起手工实现的伪对象和测试代码放在不同文件中,将它们放在一个文件里的话,定位、阅读以及维护代码都要容易的多。  

  ② 通过属性设置注入伪对象

  构造函数注入只是方法之一,属性也经常用来实现依赖注入。

  根据上图所示的流程,我们可以重构LogAnalyzer类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class LogAnalyzer
    {
        private IExtensionManager manager;

        // 允许通过属性设置依赖项
        public IExtensionManager ExtensionManager
        {
            get
            {
                return manager;
            }

            set
            {
                manager = value;
            }
        }

        public LogAnalyzer()
        {
            this.manager = new FileExtensionManager();
        }

        public bool IsValidLogFileName(string fileName)
        {
            return manager.IsValid(fileName);
        }
    }

  其次,新增一个测试方法,改为属性注入方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void IsValidFileName_SupportExtension_ReturnsTrue()
    {
        // 设置要使用的存根,确保其返回true
        FakeExtensionManager myFakeManager = new FakeExtensionManager();
        myFakeManager.WillBeValid = true;
        // 创建analyzer,注入存根
        LogAnalyzer log = new LogAnalyzer();
        log.ExtensionManager = myFakeManager;
        bool result = log.IsValidLogFileName("short.ext");

        Assert.AreEqual(true, result);
    }

Note : 如果你想表明被测试类的某个依赖项是可选的,或者测试可以放心使用默认创建的这个依赖项实例,这时你就可以使用属性注入。

1.6 抽取和重写

  抽取和重写是一项强大的技术,可直接替换依赖项,实现起来快速干净,可以让我们编写更少的接口、更多的虚函数。

  还是继续上面的例子,首先改造被测试类(位于Manulife.LogAn),添加一个返回真实实例的虚工厂方法,正常在代码中使用工厂方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class LogAnalyzerUsingFactoryMethod
    {
        public bool IsValidLogFileName(string fileName)
        {
            // use virtual method
            return GetManager().IsValid(fileName);
        }

        protected virtual IExtensionManager GetManager()
        {
            // hard code
            return new FileExtensionManager();
        }
    }

  其次,在改造测试项目(位于Manulife.LogAn.UnitTests),创建一个新类,声明这个新类继承自被测试类,创建一个我们要替换的接口(IExtensionManager)类型的公共字段(不需要属性get和set方法):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class TestableLogAnalyzer : LogAnalyzerUsingFactoryMethod
    {
        public IExtensionManager manager;

        public TestableLogAnalyzer(IExtensionManager manager)
        {
            this.manager = manager;
        }

        // 返回你指定的值
        protected override IExtensionManager GetManager()
        {
            return this.manager;
        }
    }

  最后,改造测试代码,这里我们创建的是新派生类而非被测试类的实例,配置这个新实例的公共字段,设置成我们在测试中创建的存根实例FakeExtensionManager:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void OverrideTest()
    {
        FakeExtensionManager stub = new FakeExtensionManager();
        stub.WillBeValid = true;
        // 创建被测试类的派生类的实例
        TestableLogAnalyzer logan = new TestableLogAnalyzer(stub);
        bool result = logan.IsValidLogFileName("stubfile.ext");

        Assert.AreEqual(true, result);
    }

二、交互测试-模拟对象

  工作单元可能有三种最终结果,目前为止,我们编写过的测试只针对前两种:返回值和改变系统状态。现在,我们来了解如何测试第三种最终结果-调用第三方对象。

2.1 模拟对象与存根的区别

  模拟对象和存根之间的区别很小,但二者之间的区别非常微妙,但又很重要。二者最根本的区别在于:

存根不会导致测试失败,而模拟对象可以

  下图展示了存根和模拟对象之间的区别,可以看到测试会使用模拟对象验证测试是否失败。

2.2 第一个手工模拟对象

  创建和使用模拟对象的方法与使用存根类似,只是模拟对象比存根多做一件事:它保存通讯的历史记录,这些记录之后用于预期(Expection)验证。

  假设我们的被测试项目LogAnalyzer需要和一个外部的Web Service交互,每次LogAnalyzer遇到一个过短的文件名,这个Web Service就会收到一个错误消息。遗憾的是,要测试的这个Web Service还没有完全实现。就算实现了,使用这个Web Service也会导致测试时间过长。

  因此,我们需要重构设计,创建一个新的接口,之后用于这个接口创建模拟对象。这个接口只包括我们需要调用的Web Service方法。

Step1.抽取接口,被测试代码可以使用这个接口而不是直接调用Web Service。然后创建实现接口的模拟对象,它看起来十分像存根,但是它还存储了一些状态信息,然后测试可以对这些信息进行断言,验证模拟对象是否正确调用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public interface IWebService
    {
        void LogError(string message);
    }

    public class FakeWebService : IWebService
    {
        public string LastError;
        public void LogError(string message)
        {
            this.LastError = message;
        }
    }

Step2.在被测试类中使用依赖注入(这里是构造函数注入)消费Web Service:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class LogAnalyzer
    {
        private IWebService service;

        public LogAnalyzer(IWebService service)
        {
            this.service = service;
        }

        public void Analyze(string fileName)
        {
            if (fileName.Length < 8)
            {
                // 在产品代码中写错误日志
                service.LogError(string.Format("Filename too short : {0}",fileName));
            }
        }
    }

Step3.使用模拟对象测试LogAnalyzer:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Analyze_TooShortFileName_CallsWebService()
    {
        FakeWebService mockService = new FakeWebService();
        LogAnalyzer log = new LogAnalyzer(mockService);

        string tooShortFileName = "abc.ext";
        log.Analyze(tooShortFileName);
        // 使用模拟对象进行断言
        StringAssert.Contains("Filename too short : abc.ext", mockService.LastError);
    }

  可以看出,这里的测试代码中我们是对模拟对象进行断言,而非LogAnalyzer类,因为我们测试的是LogAnalyzer和Web Service之间的交互

2.3 同时使用模拟对象和存根

  假设我们得LogAnalyzer不仅需要调用Web Service,而且如果Web Service抛出一个错误,LogAnalyzer还需要把这个错误记录在另一个外部依赖项里,即把错误用电子邮件发送给Web Service管理员,如下代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    if (fileName.Length < 8)
    {
        try
        {
            // 在产品代码中写错误日志
            service.LogError(string.Format("Filename too short : {0}", fileName));
        }
        catch (Exception ex)
        {
            email.SendEmail("a", "subject", ex.Message);
        }
    }

  可以看出,这里LogAnalyzer有两个外部依赖项:Web Service和电子邮件服务。我们看到这段代码只包含调用外部对象的逻辑,没有返回值,也没有系统状态的改变,那么我们如何测试当Web Service抛出异常时LogAnalyzer正确地调用了电子邮件服务呢?

  我们可以在测试代码中使用存根替换Web Service来模拟异常,然后模拟邮件服务来检查调用。测试的内容是LogAnalyzer与其他对象的交互。

Step1.抽取Email接口,封装Email类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public interface IEmailService
    {
        void SendEmail(EmailInfo emailInfo);
    }

    public class EmailInfo
    {
        public string Body;
        public string To;
        public string Subject;

        public EmailInfo(string to, string subject, string body)
        {
            this.To = to;
            this.Subject = subject;
            this.Body = body;
        }

        public override bool Equals(object obj)
        {
            EmailInfo compared = obj as EmailInfo;

            return To == compared.To && Subject == compared.Subject 
                && Body == compared.Body;
        }
    }

Step2.封装EmailInfo类,重写Equals方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class EmailInfo
    {
        public string Body;
        public string To;
        public string Subject;

        public EmailInfo(string to, string subject, string body)
        {
            this.To = to;
            this.Subject = subject;
            this.Body = body;
        }

        public override bool Equals(object obj)
        {
            EmailInfo compared = obj as EmailInfo;

            return To == compared.To && Subject == compared.Subject 
                && Body == compared.Body;
        }
    }

Step3.创建FakeEmailService模拟对象,改造FakeWebService为存根

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class FakeEmailService : IEmailService
    {
        public EmailInfo email = null;

        public void SendEmail(EmailInfo emailInfo)
        {
            this.email = emailInfo;
        }
    }

    public class FakeWebService : IWebService
    {
        public Exception ToThrow;
        public void LogError(string message)
        {
            if (ToThrow != null)
            {
                throw ToThrow;
            }
        }
    }

Step4.改造LogAnalyzer类适配两个Service

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public class LogAnalyzer
    {
        private IWebService webService;
        private IEmailService emailService;

        public LogAnalyzer(IWebService webService, IEmailService emailService)
        {
            this.webService = webService;
            this.emailService = emailService;
        }

        public void Analyze(string fileName)
        {
            if (fileName.Length < 8)
            {
                try
                {
                    webService.LogError(string.Format("Filename too short : {0}", fileName));
                }
                catch (Exception ex)
                {
                    emailService.SendEmail(new EmailInfo("someone@qq.com", "can't log", ex.Message));
                }
            }
        }
    }

Step5.编写测试代码,创建预期对象,并使用预期对象断言所有的属性

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Analyze_WebServiceThrows_SendsEmail()
    {
        FakeWebService stubService = new FakeWebService();
        stubService.ToThrow = new Exception("fake exception");
        FakeEmailService mockEmail = new FakeEmailService();

        LogAnalyzer log = new LogAnalyzer(stubService, mockEmail);
        string tooShortFileName = "abc.ext";
        log.Analyze(tooShortFileName);
        // 创建预期对象
        EmailInfo expectedEmail = new EmailInfo("someone@qq.com", "can't log", "fake exception");
        // 用预期对象同时断言所有属性
        Assert.AreEqual(expectedEmail, mockEmail.email);
    }

总结:每个测试应该只测试一件事情,测试中应该也最多只有一个模拟对象。一个测试只能指定工作单元三种最终结果中的一个,不然的话天下大乱。

三、隔离(模拟)框架

3.1 为何使用隔离框架

  对于复杂的交互场景,可能手工编写模拟对象和存根就会变得很不方便,因此,我们可以借助隔离框架来帮我们在运行时自动生成存根和模拟对象。

一个隔离框架是一套可编程的API,使用这套API创建伪对象比手工编写容易得多,快得多,而且简洁得多。

  隔离框架的主要功能就在于帮我们生成动态伪对象,动态伪对象是运行时创建的任何存根或者模拟对象,它的创建不需要手工编写代码(硬编码)。

3.2 关于NSubstitute隔离框架

  Nsubstitute是一个开源的框架,源码是C#实现的。你可以在这里获得它的源码:https://github.com/nsubstitute/NSubstitute

  NSubstitute 更注重替代(Substitute)概念。它的设计目标是提供一个优秀的测试替代的.NET模拟框架。它是一个模拟测试框架,用最简洁的语法,使得我们能够把更多的注意力放在测试工作,减轻我们的测试配置工作,以满足我们的测试需求,帮助完成测试工作。它提供最经常需要使用的测试功能,且易于使用,语句更符合自然语言,可读性更高。对于单元测试的新手或只专注于测试的开发人员,它具有简单、友好的语法,使用更少的lambda表达式来编写完美的测试程序。

  NSubstitute 采用的是Arrange-Act-Assert测试模式,你只需要告诉它应该如何工作,然后断言你所期望接收到的请求,就大功告成了。因为你有更重要的代码要编写,而不是去考虑是需要一个Mock还是一个Stub。

  在.NET项目中,我们仍然可以通过NuGet来安装NSubsititute:

3.3 使用NSubstitute模拟对象

  NSub是一个受限框架,它最适合为接口创建伪对象。我们继续以前的例子,来看下面一段代码,它是一个手写的伪对象FakeLogger,它会检查日志调用是否正确执行。此处我们没有使用隔离框架。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public interface ILogger
    {
        void LogError(string message);
    }

    public class FakeLogger : ILogger
    {
        public string LastError;
        public void LogError(string message)
        {
            LastError = message;
        }
    }

    

    [Test]
    public void Analyze_TooShortFileName_CallLogger()
    {
        // 创建伪对象
        FakeLogger logger = new FakeLogger();
        MyLogAnalyzer analyzer = new Chapter5.MyLogAnalyzer(logger);
        analyzer.MinNameLength = 6;
        analyzer.Analyze("a.txt");

        StringAssert.Contains("too short", logger.LastError);
    }

  现在我们看看如何使用NSub伪造一个对象,换句话说,之前我们手动写的FakeLogger在这里就不用再手动写了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Analyze_TooShortFileName_CallLogger()
    {
        // 创建模拟对象,用于测试结尾的断言
        ILogger logger = Substitute.For<ILogger>();
        MyLogAnalyzer analyzer = new MyLogAnalyzer(logger);
        analyzer.MinNameLength = 6;
        analyzer.Analyze("a.txt");

        // 使用NSub API设置预期字符串
        logger.Received().LogError("Filename too short : a.txt");
    }

  需要注意的是:

  (1)ILogger接口自身并没有这个Received方法;

  (2)NSub命名空间提供了一个扩展方法Received,这个方法可以断言在测试中调用了伪对象的某个方法;

  (3)通过在LogError()前调用Received(),其实是NSub在询问伪对象的这个方法是否调用过。

3.4 使用NSubstitute模拟值

  如果接口的方法返回不为空,如何从实现接口的动态伪对象返回一个值呢?我们可以借助NSub强制方法返回一个值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Returns_ByDefault_WorksForHardCodeArgument()
    {
        IFileNameRules fakeRules = Substitute.For<IFileNameRules>();
        // 强制方法返回假值
        fakeRules.IsValidLogFileName("strict.txt").Returns(true);

        Assert.IsTrue(fakeRules.IsValidLogFileName("strict.txt"));
    }

  如果我们不想关心方法的参数,即无论参数是什么,方法应该总是返回一个价值,这样的话测试会更容易维护,因此我们可以借助NSub的参数匹配器:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Returns_ByDefault_WorksForAnyArgument()
    {
        IFileNameRules fakeRules = Substitute.For<IFileNameRules>();
        // 强制方法返回假值
        fakeRules.IsValidLogFileName(Arg.Any<string>()).Returns(true);

        Assert.IsTrue(fakeRules.IsValidLogFileName("anything.txt"));
    }

  Arg.Any<Type>称为参数匹配器,在隔离框架中被广泛使用,控制参数处理。

  如果我们需要模拟一个异常,也可以借助NSub来解决:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Returns_ArgAny_Throws()
    {
        IFileNameRules fakeRules = Substitute.For<IFileNameRules>();

        fakeRules.When(x => x.IsValidLogFileName(Arg.Any<string>())).
            Do(context => { throw new Exception("fake exception"); });

        Assert.Throws<Exception>(() => fakeRules.IsValidLogFileName("anything"));
    }

  这里,使用了Assert.Throws验证被测试方法确实抛出了一个异常。When和Do两个方法顾名思义代表了什么时候发生了什么事,发生了事之后要触发其他什么事。需要注意的是,这里When方法必须使用Lambda表达式。

3.5 同时使用模拟对象和存根

  这里我们在一个场景中结合使用两种类型的伪对象:一个用作存根,另一个用作模拟对象。

  继续前面的一个例子,LogAnalyzer要使用一个MailServer类和一个WebService类,这次需求有变化:如果日志对象抛出异常,LogAnalyzer需要通知Web服务,如下图所示:

  我们需要确保的是:如果日志对象抛出异常,LogAnalyzer会把这个问题通知WebService。下面是被测试类的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public interface IWebService
    {
        void Write(string message);
    }

    public class LogAnalyzerNew
    {
        private ILogger _logger;
        private IWebService _webService;

        public LogAnalyzerNew(ILogger logger, IWebService webService)
        {
            _logger = logger;
            _webService = webService;
        }

        public int MinNameLength
        {
            get; set;
        }

        public void Analyze(string fileName)
        {
            if (fileName.Length < MinNameLength)
            {
                try
                {
                    _logger.LogError(string.Format("Filename too short : {0}", fileName));
                }
                catch (Exception ex)
                {
                    _webService.Write("Error From Logger : " + ex.Message);
                }
            }
        }
    }

  现在我们借助NSubstitute进行测试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    [Test]
    public void Analyze_LoggerThrows_CallsWebService()
    {
        var mockWebService = Substitute.For<IWebService>();
        var stubLogger = Substitute.For<ILogger>();
        // 无论输入什么都抛出异常
        stubLogger.When(logger => logger.LogError(Arg.Any<string>()))
            .Do(info => { throw new Exception("fake exception"); });

        var analyzer = new LogAnalyzerNew(stubLogger, mockWebService);
        analyzer.MinNameLength = 10;
        analyzer.Analyze("short.txt");
        //验证在测试中调用了Web Service的模拟对象,调用参数字符串包含 "fake exception"
        mockWebService.Received().Write(Arg.Is<string>(s => s.Contains("fake exception")));
    }

  这里我们不需要手工实现伪对象,但是代码的可读性已经变差了,因为有一堆Lambda表达式,不过它也帮我们避免了在测试中使用方法名字符串。

四、小结

  本篇我们学习了单元测试的核心技术:存根、模拟对象以及隔离框架。使用存根可以帮助我们破除依赖,模拟对象与存根的区别主要在于存根不会导致测试失败,而模拟对象则可以。要辨别你是否使用了存根,最简单的方法是:存根永远不会导致测试失败,测试总是对被测试类进行断言。使用隔离框架,测试代码会更加易读、易维护,重点是可以帮助我们节省不少时间编写模拟对象和存根。

参考资料

  (1)Roy Osherove 著,金迎 译,《单元测试的艺术(第2版)》

  (2)匠心十年,《NSubsititue完全手册

  (3)张善友,《单元测试模拟框架:NSubstitute

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-05-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
重温《单元测试的艺术》,总结常用知识点
前几个月重温了单元测试的艺术。毕竟是14年的书内容有点旧,于是试着结合书中的内容和一些新的知识点写进这篇文章,希望对自己及各位读者有帮助。
dino.c
2019/07/30
1.6K0
重温《单元测试的艺术》,总结常用知识点
.NET单元测试的艺术-3.测试代码
开篇:上一篇我们学习单元测试和核心技术:存根、模拟对象和隔离框架,它们是我们进行高质量单元测试的技术基础。本篇会集中在管理和组织单元测试的技术,以及如何确保在真实项目中进行高质量的单元测试。
Edison Zhou
2018/08/20
5730
.NET单元测试的艺术-3.测试代码
.NET单元测试的艺术-1.入门
开篇:最近在看Roy Osherove的《单元测试的艺术》一书,颇有收获。因此,将其记录下来,并分为四个部分分享成文,与各位Share。本篇作为入门,介绍了单元测试的基础知识,例如:如何使用一个测试框架,基本的自动化测试属性等等,还有对应的三种测试类型。相信你可以对编写单元测试从一无所知到及格水平,这也是原书作者的目标。
Edison Zhou
2018/08/21
2.2K0
.NET单元测试的艺术-1.入门
.NET Core下的日志(2):日志模型详解
NET Core的日志模型主要由三个核心对象构成,它们分别是Logger、LoggerProvider和LoggerFactory。总的来说,LoggerProvider提供一个具体的Logger对象将格式化的日志消息写入相应的目的地,但是我们在编程过程中使用的Logger对象则由LoggerFactory创建,这个Logger利用注册到LoggerFactory的LoggerProvider来提供真正具有日志写入功能的Logger,并委托后者来记录日志。 目录 一、Logger     扩展方法Log
蒋金楠
2018/01/15
1K0
.NET Core下的日志(2):日志模型详解
.NET Core的日志[1]:采用统一的模式记录日志
记录各种级别的日志是所有应用不可或缺的功能。关于日志记录的实现,我们有太多第三方框架可供选择,比如Log4Net、NLog、Loggr和Serilog 等,当然我们还可以选择微软原生的诊断框架(相关API定义在命名空间“System.Diagnostics”中)实现对日志的记录。.NET Core提供了独立的日志模型使我们可以采用统一的API来完成针对日志记录的编程,我们同时也可以利用其扩展点对这个模型进行定制,比如可以将上述这些成熟的日志框架整合到我们的应用中。 目录 一、日志模型三要素 二、将日志写入
蒋金楠
2018/01/15
1.1K0
.NET Core的日志[1]:采用统一的模式记录日志
ASP.NET Core 依赖注入基本用法
ASP.NET Core从框架层对依赖注入提供支持。也就是说,如果你不了解依赖注入,将很难适应 ASP.NET Core的开发模式。本文将介绍依赖注入的基本概念,并结合代码演示如何在 ASP.NET Core中使用依赖注入。
拓荒者IT
2019/09/24
2.2K0
ASP.NET Core 依赖注入基本用法
.NET Core开发实战(第28课:工作单元模式(UnitOfWork):管理好你的事务)--学习笔记
这两个方法的区别是:一个是返回的 int 是指我们影响的数据条数,另外一个返回 bool 表示我们保存是否成功,本质上这两个方法达到的效果是相同的
郑子铭
2021/01/13
3.7K0
为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]
ASP.NET Core具有一个以ServiceCollection和ServiceProvider为核心的依赖注入框架,虽然这只是一个很轻量级的框架,但是在大部分情况下能够满足我们的需要。不过我觉得它最缺乏的是针对AOP的支持,虽然这个依赖注入框架提供了扩展点使我们可以很容易地实现与第三方框架的集成,但是我又不想“节外生枝”,为此我们趁这个周末写了一个简单的Interception框架来解决这个问题。通过这个命名为Dora.Interception的框架,我们可以采用一种非常简单、直接而优雅地(呵呵)在这
蒋金楠
2018/02/08
1.1K0
为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]
ASP.NET Core 奇淫技巧之伪属性注入
开局先唠嗑一下,许久未曾更新博客,一直在调整自己的状态,去年是我的本命年,或许是应验了本命年的多灾多难,过得十分不顺,不论是生活上还是工作上。还好当我度过了所谓的本命年后,许多事情都在慢慢变好,我将会开始恢复更新博客,争取恢复到以前的速度上(因为工作比较忙,所以这个过程可能需要一段时间)。
晓晨
2020/04/01
9240
ASP.NET Core 奇淫技巧之伪属性注入
ASP.NET Core如何在ActionFilterAttribute里做依赖注入
在ASP.NET Core里,我们可以使用构造函数注入很方便地对Controller,ViewComponent等部件做依赖注入。但是如何给过滤器ActionFilterAttribute也用上构造函数注入呢?
Edi Wang
2019/07/08
1.5K0
ASP.NET Core如何在ActionFilterAttribute里做依赖注入
Mockito模拟进行单元测试
    MOCK意思是模拟的意思,主要被用来进行数据的人工组织,不会真正地调用第三方服务器,类似redis,mysql等都不会调用,也不用关心数据底层是如何进行处理的,我们要做的只是将本单元的逻辑进行单元测试,验证数据的逻辑处理性,而其中mock较好的框架就是Mockito。
chinotan
2019/07/15
9.7K0
Mockito模拟进行单元测试
.NET使用Moq开源模拟库简化单元测试
Moq是一个.NET开源、流行、使用简单的 .NET 模拟库,充分利用了.NET 的 Linq 表达式树和 lambda 表达式。这使得 Moq 成为最具生产力、类型安全且支持重构的模拟库。它不仅支持模拟接口,还支持模拟类。其 API 非常简单直观,不需要任何关于模拟概念的事先知识或经验。从而简化单元测试中的依赖管理和验证过程,提高代码的可测试性和可维护性。
追逐时光者
2024/10/29
2900
.NET使用Moq开源模拟库简化单元测试
.NET使用Moq开源模拟库简化单元测试
Moq是一个.NET开源、流行、使用简单的 .NET 模拟库,充分利用了.NET 的 Linq 表达式树和 lambda 表达式。这使得 Moq 成为最具生产力、类型安全且支持重构的模拟库。它不仅支持模拟接口,还支持模拟类。其 API 非常简单直观,不需要任何关于模拟概念的事先知识或经验。从而简化单元测试中的依赖管理和验证过程,提高代码的可测试性和可维护性。
郑子铭
2024/11/23
2570
.NET使用Moq开源模拟库简化单元测试
【愚公系列】2023年09月 .NET/C#知识点-自定义绑定模型(文件上传)
在ASP.NET MVC中,数据模型绑定是将HTTP请求数据(如表单数据)转换为CLR对象的过程。自定义绑定模型可以让我们通过自定义规则来控制数据模型绑定。以下是一些自定义绑定模型的示例:
愚公搬代码
2025/05/31
540
【愚公系列】2023年09月 .NET/C#知识点-自定义绑定模型(文件上传)
.NET重构—单元测试的代码重构
阅读目录: 1.开篇介绍 2.单元测试、测试用例代码重复问题(大量使用重复的Mock对象及测试数据) 2.1.单元测试的继承体系(利用超类来减少Mock对象的使用) 2.1.1.公用的MOCK对象
王清培
2018/01/08
1.4K0
.NET重构—单元测试的代码重构
在asp.net web api 2 (ioc autofac) 使用 Serilog 记录日志
Serilog是.net里面非常不错的记录日志的库,另外一个我认为比较好的Log库是NLog。 在我个人的asp.net web api 2 基础框架(Github地址)里,我原来使用的是NLog,但是由于好奇心,我决定使用Serilog代替Nlog。 安装: 首先安装 Serilog,通过Package Manager Console或者Nuget管理窗口进行安装: PM> Install-Package Serilog 然后安装 Serilog的Sinks,所谓Sink就是记录Log的途径,比如在控制台
solenovex
2018/03/01
2.2K0
5 分钟 .NET 单元测试极简入门
为什么要花时间写单元测试?我直接让测试团队人肉测试,然后直接上生产,有什么问题吗?
郑子铭
2023/08/30
5530
5 分钟 .NET 单元测试极简入门
从头编写 asp.net core 2.0 web api 基础框架 (3)
Github源码地址:https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratc
solenovex
2018/03/01
1.6K0
从头编写 asp.net core 2.0 web api 基础框架 (3)
【愚公系列】2023年02月 WMS智能仓储系统-010.全局过滤、中间件、格式化配置
ServiceFilter 我们必须在 Startup.cs 中注册.TypeFilter 由 Microsoft.Extensions.DependencyInjection.ObjectFactory 注入,我们不需要注册该过滤器。
愚公搬代码
2023/02/16
5550
.NET Core下的日志(1):记录日志信息
记录各种级别的日志是所有应用不可或缺的功能。关于日志记录的实现,我们有太多第三方框架可供选择,比如Log4Net、NLog、Loggr和Serilog 等,当然我们还可以选择微软原生的诊断机制(相关API定义在命名空间“System.Diagnostics”中)实现对日志的记录。.NET Core提供了独立的日志模型使我们可以采用统一的API来完成针对日志记录的编程,我们同时也可以利用其扩展点对这个模型进行定制,比如可以将上述这些成熟的日志框架整合到我们的应用中。本系列文章旨在从设计和实现的角度对.NET
蒋金楠
2018/01/15
1.3K0
.NET Core下的日志(1):记录日志信息
推荐阅读
相关推荐
重温《单元测试的艺术》,总结常用知识点
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验