C# 单元测试,带你快速入门

注:本文示例环境

  • VS2017
  • XUnit 2.2.0 单元测试框架
  • xunit.runner.visualstudio 2.2.0 测试运行工具
  • Moq 4.7.10 模拟框架

为什么要编写单元测试

对于为什么要编写单元测试,我想每个人都有着自己的理由。对于我个人来说,主要是为了方便修改(bug修复)而不引入新的问题。可以放心大胆的重构,我认为重构觉得是提高代码质量和提升个人编码能力的一个非常有用的方式。好比一幅名画一尊雕像,都是作者不断重绘不断打磨出来的,而优秀的代码也需要不断的重构。 当然好处不仅仅如此。TDD驱动,使代码更加注重接口,迫使代码减少耦合,使开发人员一开始就考虑面对各种情况编写代码,一定程度的保证的代码质量,通过测试方法使后续人员快速理解代码...等。 额,至于不写单元测试的原因也有很多。原因无非就两种:懒、不会。当然你还会找更多的理由的。

框架选型

至于框架的选型。其实本人并不了解也没写过单元测试,这算是第一次真正接触吧。在不了解的情况下怎么选型呢?那就是看哪个最火、用的人多就选哪个。起码出了问题也容易同别人交流。

  • 单元测试框架:XUnit 2.2.0。asp.net mvc就是用的这个,此内框架还有:NUnit、MSTest等。
  • 测试运行工具:xunit.runner.visualstudio 2.2.0。类似如:Resharper的xUnit runner插件。
  • 模拟框架:Moq 4.7.10。 asp.net mvc、Orchard使用了。此类框架还有:RhinoMocks、NSubstitute、FakeItEasy等。

基本概念

  • AAA逻辑顺序
    • 准备(Arrange)对象,创建对象,进行必要的设置
    • 操作(Act)对象
    • 断言(Assert)某件事情是预期的。
  • Assert(断言):对方法或属性的运行结果进行检测
  • Stub(测试存根\桩对象):用返回指定结果的代码替换方法(去伪造一个方法,阻断对原来方法的调用,为了让测试对象可以正常的执行)
  • Mock(模拟对象):一个带有期望方法被调用的存根(可深入的模拟对象之间的交互方式,如:调用了几次、在某种情况下是否会抛出异常。mock是一种功能丰富的stub) Stub和Mock的定义比较抽象不好理解,延伸阅读1阅读2阅读3

好的测试

  • 测试即文档
  • 无限接近言简意赅的自然化语言
  • 测试越简明越好,每个测试只关注一个点。
  • 好的测试足够快,测试易于编写,减少依赖
  • 好的测试应该相互隔离,不依赖于别的测试,不依赖于外部资源
  • 可描述的命名:UnitOfWorkName_ScenarioUnderTest_ExpectedBehavior(命名可团队约定,我甚至觉得中文命名也没什么不可以的)
    • UnitOfWorkName  被测试的方法、一组方法或者一组类
    • Scenario  测试进行的假设条件,例如“登入失败”,“无效用户”或“密码正确”等
    • ExpectedBehavior  在测试场景指定的条件下,你对被测试方法行为的预期  

基础实践

“废话”说的够多了,下面撸起袖子开干吧。 下面开始准备工作:

  • vs2017新建一个空项目 UnitTestingDemo
  • 新建类库 TestDemo (用于编写被测试的类)
  • 新建类库 TestDemo.Tests (用于编写单元测试)
  • 对类库 TestDemo.Tests 用nuget 安装XUnit 2.2.0、xunit.runner.visualstudio 2.2.0、Moq 4.7.10。
  • 添加 TestDemo.Tests 对 TestDemo 的引用。

例:

public class Arithmetic
{
    public int Add(int nb1, int nb2)
    {
        return nb1 + nb2;
    }
}

对应的单元测试:(需要导入using Xunit;命名空间。 )

public class Arithmetic_Tests
{
    [Fact]//需要在测试方法加上特性Fact
    public void Add_Ok()
    {
        Arithmetic arithmetic = new Arithmetic();
        var sum = arithmetic.Add(1, 2);

        Assert.True(sum == 3);//断言验证
    }
}

一个简单的测试写好了。由于我们使用的vs2017 它出了一个新的功能“Live Unit Testing”,我们可以启用它进行实时的测试。也就是我们编辑单元测试,然后保存的时候,它会自动生成自动测试,最后得出结果。 我们看到了验证通过的绿色√。 注意到测试代码中的参数和结果都写死了。如果我们要对多种情况进行测试,岂不是需要写多个单元测试方法或者进行多次方法执行和断言。这也太麻烦了。在XUnit框架中为我们提供了Theory特性。使用如下: 例:

[Theory]
[InlineData(2, 3, 5)]
[InlineData(2, 4, 6)]
[InlineData(2, 1, 3)] //对应测试方法的形参
public void Add_Ok_Two(int nb1, int nb2, int result)
{
    Arithmetic arithmetic = new Arithmetic();
    var sum = arithmetic.Add(nb1, nb2);
    Assert.True(sum == result);
}

测试了正确的情况,我们也需要测试错误的情况。达到更好的覆盖率。 例:

[Theory]
[InlineData(2, 3, 0)]
[InlineData(2, 4, 0)]
[InlineData(2, 1, 0)]
public void Add_No(int nb1, int nb2, int result)
{
    Arithmetic arithmetic = new Arithmetic();
    var sum = arithmetic.Add(nb1, nb2);
    Assert.False(sum == result);
}

有时候我们需要确定异常 例:

public int Divide(int nb1, int nb2)
{
    if (nb2==0)
    {
        throw new Exception("除数不能为零");
    }
    return nb1 / nb2;
}
[Fact]
public void Divide_Err()
{
    Arithmetic arithmetic = new Arithmetic();
    Assert.Throws<Exception>(() => { arithmetic.Divide(4, 0); });//断言 验证异常
}

以上为简单的单元测试。接下来,我们讨论更实际更真实的。 我们一般的项目都离不开数据库操作,下面就来实践下对EF使用的测试:

  • 使用nuget安装 EntityFramework 5.0.0

例:

public class StudentRepositories
{
    //...
    public void Add(Student model)
    {
        db.Set<Student>().Add(model);
        db.SaveChanges();
    }
}
[Fact]
public void Add_Ok()
{
    StudentRepositories r = new StudentRepositories();
    Student student = new Student()
    {
        Id = 1,
        Name = "张三"
    };
    r.Add(student);

    var model = r.Students.Where(t => t.Name == "张三").FirstOrDefault();
    Assert.True(model != null);
}

我们可以看到我们操作的是EF连接的实际库。(注意:要改成专用的测试库) 我们会发现,每测试一次都会产生对应的垃圾数据,为了避免对测试的无干扰性。我们需要对每次测试后清除垃圾数据。

//注意:测试类要继承IDisposable接口
public void Dispose()
{
 StudentRepositories r = new StudentRepositories();
 var models = r.Students.ToList();
 foreach (var item in models)
 {
     r.Delete(item.Id);
 }
}

这样每执行一个测试方法就会对应执行一次Dispose,可用来清除垃圾数据。 我们知道对数据库的操作是比较耗时的,而单元测试的要求是尽可能的减少测试方法的执行时间。因为单元测试执行的比较频繁。基于前面已经对数据库的实际操作已经测试过了,所以我们在后续的上层操作使用Stub(存根)来模拟,而不再对数据库进行实际操作。 例: 我们定义一个接口IStudentRepositories 并在StudentRepositories 继承。

 public interface IStudentRepositories
 {
     void Add(Student model);
 }
 public class StudentRepositories: IStudentRepositories
 {
    //省略。。。 (还是原来的实现)
 }   
public class StudentService
{
    IStudentRepositories studentRepositories;
    public StudentService(IStudentRepositories studentRepositories)
    {
        this.studentRepositories = studentRepositories;
    }
    public bool Create(Student student)
    {
        studentRepositories.Add(student);

        return true;
    }
}

新建一个类,用来测试。这个Create会使用仓储操作数据库。这里不希望实际操作数据库,以达到快速测试执行。

[Fact]
public void Create_Ok()
{
    IStudentRepositories studentRepositories = new StubStudentRepositories();
    StudentService service = new StudentService(studentRepositories);
    var isCreateOk = service.Create(null);
    Assert.True(isCreateOk);
}

public class StubStudentRepositories : IStudentRepositories
{
    public void Add(Student model)
    {
    }
}

图解: 每次做类似的操作都要手动建议StubStudentRepositories存根,着实麻烦。好在Mock框架(Moq)可以自动帮我们完成这个步骤。 例:

[Fact]
public void Create_Mock_Ok()
{
    var studentRepositories = new Mock<IStudentRepositories>();
    var notiy = new Mock<Notiy>();
    StudentService service = new StudentService(studentRepositories.Object);
    var isCreateOk = service.Create(null);
    Assert.True(isCreateOk);
}

相比上面的示例,是不是简化多了。起码代码看起来清晰了,可以更加注重测试逻辑。 下面接着来看另外的情况,并且已经通过了测试

public class Notiy
{
    public bool Info(string messg)
    {
        //发送消息、邮件发送、短信发送。。。
        //.........
        if (string.IsNullOrWhiteSpace(messg))
        {
            return false;
        }
        return true;
    }
}
public class Notiy_Tests
{
    [Fact]
    public void Info_Ok()
    {
        Notiy notiy = new Notiy();
        var isNotiyOk = notiy.Info("消息发送成功");
        Assert.True(isNotiyOk);
    }
}

现在我们接着前面的Create方法加入消息发送逻辑。

public bool Create(Student student)
{
    studentRepositories.Add(student);

    var isNotiyOk = notiy.Info("" + student.Name);//消息通知

    //其他一些逻辑
    return isNotiyOk;
}
[Fact]
public void Create_Mock_Notiy_Ok()
{
    var studentRepositories = new Mock<IStudentRepositories>();
    var notiy = new Mock<Notiy>();
    StudentService service = new StudentService(studentRepositories.Object, notiy.Object);
    var isCreateOk = service.Create(new Student());
    Assert.True(isCreateOk);
}

而前面我们已经对Notiy进行过测试了,接下来我们不希望在对Notiy进行耗时操作。当然,我们可以通过上面的Mock框架来模拟。这次和上面不同,某些情况我们不需要或不想写对应的接口怎么来模拟?那就使用另外一种方式把要测试的方法virtual。 例:

public virtual bool Info(string messg)
{
    //发送消息、邮件发送、短信发送。。。
    //.........
    if (string.IsNullOrWhiteSpace(messg))
    {
        return false;
    }
    return true;
}

测试如下

[Fact]
public void Create_Mock_Notiy_Ok()
{
    var studentRepositories = new Mock<IStudentRepositories>();
    var notiy = new Mock<Notiy>();
    notiy.Setup(f => f.Info(It.IsAny<string>())).Returns(true);//【1】
    StudentService service = new StudentService(studentRepositories.Object, notiy.Object);
    var isCreateOk = service.CreateAndNotiy(new Student());
    Assert.True(isCreateOk);
}

我们发现了标注【1】处的不同,这个代码的意思是,执行模拟的Info方法返回值为true。参数It.IsAny() 是任意字符串的意思。 当然你也可以对不同参数给不同的返回值:

notiy.Setup(f => f.Info("")).Returns(false);
notiy.Setup(f => f.Info("消息通知")).Returns(true);

有时候我们还需要对private方法进行测试

  • 使用nuget 安装 MSTest.TestAdapter 1.1.17
  • 使用nuget 安装 MSTest.TestFramework 1.1.17

例:

private bool XXXInit()
{
    return true;
}
[Fact]
public void XXXInit_Ok()
{
    var studentRepositories = new StudentService();
    var obj = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject(studentRepositories);
    Assert.True((bool)obj.Invoke("XXXInit"));
}

如果方法有参数,接着Invoke后面传入即可。

好了,就说这么多吧。只能说测试的内容还真多,想要一篇文章说完是不可能的。但希望已经带你入门了。

附录

xUnit(2.0) 断言 (来源)

  • Assert.Equal() 验证两个参数是否相等,支持字符串等常见类型。同时有泛型方法可用,当比较泛型类型对象时使用默认的IEqualityComparer实现,也有重载支持传入IEqualityComparer
  • Assert.NotEqual() 与上面的相反
  • Assert.Same() 验证两个对象是否同一实例,即判断引用类型对象是否同一引用
  • Assert.NotSame() 与上面的相反
  • Assert.Contains() 验证一个对象是否包含在序列中,验证一个字符串为另一个字符串的一部分
  • Assert.DoesNotContain() 与上面的相反
  • Assert.Matches() 验证字符串匹配给定的正则表达式
  • Assert.DoesNotMatch() 与上面的相反
  • Assert.StartsWith() 验证字符串以指定字符串开头。可以传入参数指定字符串比较方式
  • Assert.EndsWith() 验证字符串以指定字符串结尾
  • Assert.Empty() 验证集合为空
  • Assert.NotEmpty() 与上面的相反
  • Assert.Single() 验证集合只有一个元素
  • Assert.InRange() 验证值在一个范围之内,泛型方法,泛型类型需要实现IComparable,或传入IComparer
  • Assert.NotInRange() 与上面的相反
  • Assert.Null() 验证对象为空
  • Assert.NotNull() 与上面的相反
  • Assert.StrictEqual() 判断两个对象严格相等,使用默认的IEqualityComparer对象
  • Assert.NotStrictEqual() 与上面相反
  • Assert.IsType()/Assert.IsType() 验证对象是某个类型(不能是继承关系)
  • Assert.IsNotType()/Assert.IsNotType() 与上面的相反
  • Assert.IsAssignableFrom()/Assert.IsAssignableFrom() 验证某个对象是指定类型或指定类型的子类
  • Assert.Subset() 验证一个集合是另一个集合的子集
  • Assert.ProperSubset() 验证一个集合是另一个集合的真子集
  • Assert.ProperSuperset() 验证一个集合是另一个集合的真超集
  • Assert.Collection() 验证第一个参数集合中所有项都可以在第二个参数传入的Action序列中相应位置的Action上执行而不抛出异常。
  • Assert.All() 验证第一个参数集合中的所有项都可以传入第二个Action类型的参数而不抛出异常。与Collection()类似,区别在于这里Action只有一个而不是序列。
  • Assert.PropertyChanged() 验证执行第三个参数Action使被测试INotifyPropertyChanged对象触发了PropertyChanged时间,且属性名为第二个参数传入的名称。
  • Assert.Throws()/Assert.Throws()Assert.ThrowsAsync()/Assert.ThrowsAsync() 验证测试代码抛出指定异常(不能是指定异常的子类)如果测试代码返回Task,应该使用异步方法
  • Assert.ThrowsAny() 验证测试代码抛出指定异常或指定异常的子类
  • Assert.ThrowsAnyAsync() 如果测试代码返回Task,应该使用异步方法

Moq(4.7.10) It参数约束

  • Is:匹配确定的给定类型
  • IsAny:匹配给定的任何值
  • IsIn: 匹配指定序列中存在的任何值
  • IsNotIn: 匹配指定序列中未找到的任何值
  • IsNotNull: 找任何值的给定值类型,除了空
  • IsInRange:匹配给定类型的范围
  • IsRegex:正则匹配

相关资料

相关推荐

demo

时间: 2024-10-01 00:23:22

C# 单元测试,带你快速入门的相关文章

10 分钟,带你快速入门前端三大技术(HTML、CSS、JavaScript)

听到前端技术,不少朋友一定会感到有些陌生.但其实,前端,你每天都在接触. 你正在使用的APP,你正在浏览的网页,这些你能看到的界面,都属于前端. 而前端最重要的三大技术,HTML,CSS,JavaScript,则是每一个前端开发者必须具备的技能. 掌握这些技能,你可以快速地做出一个酷炫的APP界面或者一个简单大方的网站页面.因此,就让我们一起来快速学习一下这三门技术吧. 以下内容节选自课程<Vue.js 和 Node.js 构建内容发布系统>. 大家也可以点击课程链接,在实验楼提供的虚拟机环境

30分钟带你快速入门MySQL教程

这是一篇真正适合初学者的MySQL数据库入门文章,哪怕你从来没有接触过数据库,或者说你从来没有听说过有数据库这东西,请一定要相信我,我当时就是这么过来的. 如果你刚开始接触MySQL数据库,或者你需要使用MySQL数据库来保存一些基本的数据,比如说,用户基本信息.学生基本信息表等,但却不知道何从下手,那么这篇文章就很适合你了,下面通过一个有趣的案例来带你熟悉MySQL的基本指令操作,希望你也能跟着操作,这样之后,相信你肯定就不会觉得很陌生了. 本文力图思路清晰和简洁,虽然有点长,但文字都是非常通

Zara带你快速入门WPF(4)---Command与功能区控件

前言:许多数据驱动的应用程序都包含菜单和工具栏或功能区控件,允许用户控制操作,在WPF中,也可以使用功能区控件,所以这里介绍菜单和功能区控件. 一.菜单控件 在WPF中,菜单很容易使用Menu和MenuItem元素创建,如下面代码,其中一个主菜单和一个次菜单,以及一个子菜单项列表. <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presen

Wireshark抓包,带你快速入门

前言 关于抓包我们平时使用的最多的可能就是Chrome浏览器自带的Network面板了(浏览器上F12就会弹出来).另外还有一大部分人使用Fiddler,Fiddler也是一款非常优秀的抓包工具.但是这两者只能对于HTTP和HTTPS进行抓包分析.如果想要对更底层的协议进行分析(如TCP的三次握手)就需要用到我们今天来说的工具Wireshark,同样是一款特牛逼的软件,且开源免费自带中文语言包. 安装和基本使用 Wireshark开源地址:https://github.com/wireshark

一篇文章带你快速入门createjs

开始用createjs这个框架的时候,发现网上的相关教程还是挺少的,所以写一篇文章,方便日后查看. createjs简介 官网:http://www.createjs.cc/ createjs中包含以下四个部分: EaselJS:用于 Sprites.动画.向量和位图的绘制,创建 HTML5 Canvas 上的交互体验(包含多点触控) TweenJS:用于做动画效果 SoundJS:音频播放引擎 PreloadJS:网站资源预加载 类似于SoundJS,PreloadJS,如果自己处理起来比较方

Zara带你快速入门WPF(4)---菜单与功能区控件

前言:许多数据驱动的应用程序都包含菜单和工具栏或功能区控件,允许用户控制操作,在WPF中,也可以使用功能区控件,所以这里介绍菜单和功能区控件. 一.菜单控件 在WPF中,菜单很容易使用Menu和MenuItem元素创建,如下面代码,其中一个主菜单和一个次菜单,以及一个子菜单项列表. <Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presen

python快速入门——进入数据挖掘你该有的基础知识

这篇文章是用来总结python中重要的语法,通过这些了解你可以快速了解一段python代码的含义 Python 的基础语法来带你快速入门 Python 语言.如果你想对 Python 有全面的了解请关注本博客Python的文章,这篇文章也可以当作复习,自己查漏补缺,读者可以在留言区分享自己的 Python 学习和使用心得. 首先聊一下Python的意义 要学好数据分析,一定要掌握 Python 吗?我想,要想学好数据分析,你最好掌握 Python 语言.为什么这么说呢? 首先,在一份关于开发语言

一文快速入门Docker

Docker提供一种安全.可重复的环境中自动部署软件的方式,拉开了基于与计算平台发展方式的变革序幕.如今Docker在互联网公司使用已经非常普遍.本文用十分钟时间,带你快速入门Docker. Docker是什么 Docker是什么? 官网首页的介绍: Enterprise Container Platform for High-Velocity Innovation. Securely build, share and run any application, anywhere 百度百科告诉我们

程序员带你学习安卓开发,十天快速入门-基础知识(四)

关注今日头条-做全栈攻城狮,学代码也要读书,爱全栈,更爱生活.提供程序员技术及生活指导干货. 如果你真想学习,请评论学过的每篇文章,记录学习的痕迹. 请把所有教程文章中所提及的代码,最少敲写三遍,达到熟悉的效果. 本系列课程是.Net程序员学习安卓开发系列课程. 下面是前三次课程列表: 程序员带你学习安卓开发,十天快速入门-安卓学习必要性 程序员带你学习安卓开发,十天快速入门-开发工具配置学习 程序员带你学习安卓开发,十天快速入-对比C#学习java语法 为了大家系统有效的快速入门安卓开发,推荐