ASP.NET 系列:单元测试

单元测试能够有效的能够在编码、设计、调试到重构等多方面显著提高咱们的工做效率和质量。github上可供参考和学习的各类开源项目众多,NopCommerce、Orchard等以及微软的asp.net mvc、entity framework相关多数项目均可以做为学习单元测试的参考。单元测试之道(C#版本)、.NET单元测试艺术C#测试驱动开发都是不错的学习资料。git

1.单元测试的好处

(1)单元测试帮助设计github

单元测试迫使咱们从关注实现转向关注接口,编写单元测试的过程就是设计接口的过程,使单元测试经过的过程是咱们编写实现的过程。我一直以为这是单元测试最重要的好处,让咱们关注的重点放在接口上而非实现的细节。服务器

(2)单元测试帮助编码mvc

应用单元测试会使咱们主动消除和减小没必要要的耦合,虽然出发点多是为了更方便的完成单元测试,但结果一般是类型的职责更加内聚,类型间的耦合显著下降。这是已知的提高编码质量的有效手段,也是提高开发人员编码水平的有效手段。框架

(3)单元测试帮助调试asp.net

应用了单元测试的代码在调试时能够快速定位问题的出处。单元测试

(4)单元测试帮助重构学习

对于现有项目的重构,从编写单元测试开始是更好的选择。先从局部代码进行重构,提取接口进行单元测试,而后再进行类型和层次级别的重构。测试

单元测试在设计、编码和调试上的做用足以使其成为软件开发相关人员的必备技能。this

2.应用单元测试

单元测试不是简单的了解使用相似XUnit和Moq这样的测试和模拟框架就可使用了,首先必须对咱们要编写的代码有足够的了解。一般咱们把代码当作一些静态的互相关联的类型,类型之间的依赖使用接口,实现类实现接口,在运行时经过自定义工厂或使用依赖注入容器管理。一个单元测试一般是在一个方法中调用要测试的方法或属性,经过使用Assert断言对方法或属性的运行结果进行检测,一般咱们须要编写的测试代码有如下几种。

(1)测试领域层

领域层由POCO组成,能够直接测试领域模型的公开行为和属性。

(2)测试应用层

应用层主要由服务接口和实现组成,应用层对基础设施组件的依赖以接口方式存在,这些基础设施的接口经过Mock方式模拟。

(3)测试表示层

表示层对应用层的依赖表如今对服务接口的调用上,经过Mock方式获取依赖接口的实例。

(4)测试基础设施层

基础设施层的测试一般涉及到配置文件、Log、HttpContext、SMTP等系统环境,一般须要使用Mock模式。

(5)使用单元测试进行集成测试

首先系统之间经过接口依赖,经过依赖注入容器获取接口实例,在配置依赖时,已经实现的部分直接配置,伪实现的部分配置为Mock框架生成的实例对象。随着系统的不断实现,不断将依赖配置的Mock对象替换为实现对象。

3.使用Assert判断逻辑行为正确性

Assert断言类是单元测试框架中的核心类,在单元测试的方法中,经过Assert类的静态方法对要测试的方法或属性的运行结果进行校验来判断逻辑行为是否正确,Should方法一般是以扩展方法形式提供的Assert的包装。

(1)Assert断言

若是你使用过System.Diagnostics.Contracts.Contract的Assert方法,那么对XUnit等单元测试框架中提供的Assert静态类会更容易,一样是条件判断,单元测试框架中的Assert类提供了大量更加具体的方法如Assert.True、Assert.NotNull、Assert.Equal等便于条件判断和信息输出。

(2)Should扩展方法

使用Should扩展方法既减小了参数的使用,又加强了语义,同时提供了更友好的测试失败时的提示信息。Xunit.should已经中止更新,Should组件复用了Xunit的Assert实现,但也已经中止更新。Shouldly组件则使用了本身实现,是目前仍在更新的项目,structuremap在单元测试中使用Shouldly。手动对Assert进行包装也很容易,下面的代码提取自 NopComnerce 3.70 中对NUnit的Assert的自定义扩展方法。

namespace Nop.Tests
{
    public static class TestExtensions
    {
        public static T ShouldNotNull<T>(this T obj)
        {
            Assert.IsNull(obj);
            return obj;
        }

        public static T ShouldNotNull<T>(this T obj, string message)
        {
            Assert.IsNull(obj, message);
            return obj;
        }

        public static T ShouldNotBeNull<T>(this T obj)
        {
            Assert.IsNotNull(obj);
            return obj;
        }

        public static T ShouldNotBeNull<T>(this T obj, string message)
        {
            Assert.IsNotNull(obj, message);
            return obj;
        }

        public static T ShouldEqual<T>(this T actual, object expected)
        {
            Assert.AreEqual(expected, actual);
            return actual;
        }

        ///<summary>
        /// Asserts that two objects are equal.
        ///</summary>
        ///<param name="actual"></param>
        ///<param name="expected"></param>
        ///<param name="message"></param>
        ///<exception cref="AssertionException"></exception>
        public static void ShouldEqual(this object actual, object expected, string message)
        {
            Assert.AreEqual(expected, actual);
        }

        public static Exception ShouldBeThrownBy(this Type exceptionType, TestDelegate testDelegate)
        {
            return Assert.Throws(exceptionType, testDelegate);
        }

        public static void ShouldBe<T>(this object actual)
        {
            Assert.IsInstanceOf<T>(actual);
        }

        public static void ShouldBeNull(this object actual)
        {
            Assert.IsNull(actual);
        }

        public static void ShouldBeTheSameAs(this object actual, object expected)
        {
            Assert.AreSame(expected, actual);
        }

        public static void ShouldBeNotBeTheSameAs(this object actual, object expected)
        {
            Assert.AreNotSame(expected, actual);
        }

        public static T CastTo<T>(this object source)
        {
            return (T)source;
        }

        public static void ShouldBeTrue(this bool source)
        {
            Assert.IsTrue(source);
        }

        public static void ShouldBeFalse(this bool source)
        {
            Assert.IsFalse(source);
        }

        /// <summary>
        /// Compares the two strings (case-insensitive).
        /// </summary>
        /// <param name="actual"></param>
        /// <param name="expected"></param>
        public static void AssertSameStringAs(this string actual, string expected)
        {
            if (!string.Equals(actual, expected, StringComparison.InvariantCultureIgnoreCase))
            {
                var message = string.Format("Expected {0} but was {1}", expected, actual);
                throw new AssertionException(message);
            }
        }
    }
}

4.使用伪对象

伪对象能够解决要测试的代码中使用了没法测试的外部依赖问题,更重要的是经过接口抽象实现了低耦合。例如经过抽象IConfigurationManager接口来使用ConfigurationManager对象,看起来彷佛只是为了单元测试而增长更多的代码,实际上咱们一般不关心后去的配置是不是经过ConfigurationManager静态类读取的config文件,咱们只关心配置的取值,此时使用IConfigurationManager既能够不依赖具体的ConfigurationManager类型,又能够在系统须要扩展时使用其余实现了IConfigurationManager接口的实现类。

使用伪对象解决外部依赖的主要步骤:

(1)使用接口依赖取代原始类型依赖。

(2)经过对原始类型的适配实现上述接口。

(3)手动建立用于单元测试的接口实现类或在单元测试时使用Mock框架生成接口的实例

手动建立的实现类完整的实现了接口,这样的实现类能够在多个测试中使用。能够选择使用Mock框架生成对应接口的实例,只须要对当前测试须要调用的方法进行模拟,一般须要根据参数进行逻辑判断,返回不一样的结果。不管是手动实现的模拟类对象仍是Mock生成的伪对象都称为桩对象,即Stub对象。Stub对象的本质是被测试类依赖接口的伪对象,它保证了被测试类能够被测试代码正常调用。

解决了被测试类的依赖问题,还须要解决没法直接在被测试方法上使用Assert断言的状况。此时咱们须要在另外一类伪对象上使用Assert,一般咱们把Assert使用的模拟对象称为模拟对象,即Mock对象。Mock对象的本质是用来提供给Assert进行验证的,它保证了在没法直接使用断言时能够正常验证被测试类。

Stub和Mock对象都是伪对象,即Fake对象

Stub或Mock对象的区分明白了就很简单,从被测试类的角度讲Stub对象,从Assert的角度讲Mock对象。然而,即便不了解相关的含义和区别也不会在使用时产生问题。好比测试邮件发送,咱们一般不能直接在被测试代码上应用Assert,咱们会在模拟的STMP服务器对象上应用Assert判断是否成功接收到邮件,这个SMTPServer模拟对象就是Mock对象而不是Stub对象。好比写日志,咱们一般能够直接在ILogger接口的相关方法上应用Assert判断是否成功,此时的Logger对象便是Stub对象也是Mock对象。

5.单元测试经常使用框架和组件

(1)单元测试框架。

XUnit是目前最为流行的.NET单元测试框架。NUnit出现的较早被普遍使用,如nopCommerce、Orchard等项目从开始就一直使用的是NUnit。XUnit目前是比NUnit更好的选择,从github上能够看到asp.net mvc等一系列的微软项目使用的就是XUnit框架。

(2)Mock框架

Moq是目前最为流行的Mock框架。Orchard、asp.net mvc等微软项目使用Moq。nopCommerce使用Rhino MocksNSubstitute和FakeItEasy是其余两种应用普遍的Mock框架。

(3)邮件发送的Mock组件netDumbster

能够经过nuget获取netDumbster组件,该组件提供了SimpleSmtpServer对象用于模拟邮件发送环境。

一般咱们没法直接对邮件发送使用Assert,使用netDumbster咱们能够对模拟服务器接收的邮件应用Assert。

public void SendMailTest()
{
    SimpleSmtpServer server = SimpleSmtpServer.Start(25);
    IEmailSender sender = new SMTPAdapter();
    sender.SendMail("sender@here.com", "receiver@there.com", "subject", "body");
    Assert.Equal(1, server.ReceivedEmailCount);
    SmtpMessage mail = (SmtpMessage)server.ReceivedEmail[0];
    Assert.Equal("sender@here.com", mail.Headers["From"]);
    Assert.Equal("receiver@there.com", mail.Headers["To"]);
    Assert.Equal("subject", mail.Headers["Subject"]);
    Assert.Equal("body", mail.MessageParts[0].BodyData);
    server.Stop();
}

(4)HttpContext的Mock组件HttpSimulator

一样能够经过nuget获取,经过使用HttpSimulator对象发起Http请求,在其生命周期内HttContext对象为可用状态。

因为HttpContext是封闭的没法使用Moq模拟,一般咱们使用以下代码片段:

private HttpContext SetHttpContext()
{
    HttpRequest httpRequest = new HttpRequest("", "http://mySomething/", "");
    StringWriter stringWriter = new StringWriter();
    HttpResponse httpResponse = new HttpResponse(stringWriter);
    HttpContext httpContextMock = new HttpContext(httpRequest, httpResponse);
    HttpContext.Current = httpContextMock;
    return HttpContext.Current;
}

使用HttpSimulator后咱们能够简化代码为:

using (HttpSimulator simulator = new HttpSimulator())
{
  
}

这对使用IoC容器和EntityFramework的程序的DbContext生命周期的测试十分重要,DbContext的生命周期必须和HttpRequest一致,所以对IoC容器进行生命周期的测试是必须的。

6.使用单元测试的难处

(1)不肯意付出学习成本和改变现有开发习惯。

(2)没有思考的习惯,错误的把单元测试当框架学。

(3)在项目后期才应用单元测试,即获取不到单元测试的好处又由于代码的测试不友好对单元测试产生误解。

(4)拒绝考虑效率、扩展性和解耦,只考虑数据和功能的实现。

相关文章
相关标签/搜索