C# 单元测试 交互测试-模拟对象

二、交互测试-模拟对象

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

2.1 模拟对象与存根的区别

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

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

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

2.2 第一个手工模拟对象

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

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

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

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

    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:

    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:

    [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管理员,如下代码所示:

    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类

    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方法

    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为存根

    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

    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.编写测试代码,创建预期对象,并使用预期对象断言所有的属性

    [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);
    }

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

 

 

 

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

posted @ 2020-05-30 16:28  delafqm  阅读(408)  评论(0编辑  收藏  举报