注:本文示例环境

  • 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

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

  1. 可能是史上最强大的js图表库——ECharts带你入门

    PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 StanZhai 两位仁兄让我试试 ECharts ,去主页看到<Why ECharts ?&g ...

  2. 史上最强大的js图表库——ECharts带你入门(转)

    出处:http://www.cnblogs.com/zrtqsk/p/4019412.html PS:之前的那篇博客Highcharts——让你的网页上图表画的飞起 ,评论中,花儿笑弯了腰 和 Sta ...

  3. SQLite 带你入门

    SQLite数据库相较于我们常用的Mysql,Oracle而言,实在是轻量得不行(最低只占几百K的内存).平时开发或生产环境中使用各种类型的数据库,可能都需要先安装数据库服务(server),然后才能 ...

  4. 一天带你入门到放弃vue.js(三)

    自定义指令 在上面学习了自定义组件接下来看一下自定义指令 自己新建的标签赋予特殊功能的是组件,而指定是在标签上使用类似于属性,以v-name开头,v-on,v-if...是系统指令! v-是表示这是v ...

  5. 一天带你入门到放弃vue.js(二)

    接下来我们继续学习一天带你入门到放弃系列vue.js(二),如有问题请留言讨论! v-if index.html <div id="app"> <p v-if=& ...

  6. 一天带你入门到放弃vue.js(一)

    写在前面的话! 每个新的框架入手都会进行一些列的扯犊子!这里不多说那么多!简简单单说一下vue吧! Vue.js是目前三大框架(angular,vue,react)之一,是渐进式js框架,据说是摒弃了 ...

  7. net core体系-web应用程序-4net core2.0大白话带你入门-1目录

    asp.net core2.0大白话带你入门 本系列包括: 1.新建asp.net core项目2.web项目目录解读3.配置访问地址4.环境变量详解5.配置文件6.日志7.DI容器8.服务的生命周期 ...

  8. 一个有趣的小例子,带你入门协程模块-asyncio

    一个有趣的小例子,带你入门协程模块-asyncio 上篇文章写了关于yield from的用法,简单的了解异步模式,[https://www.cnblogs.com/c-x-a/p/10106031. ...

  9. 带你入门SpringCloud统一配置 | SpringCloud Config

    前言 在微服务中众多服务的配置必然会出现相同的配置,如果配置发生变化需要修改,一个个去修改然后重启项目的方案是绝对不可取的.而 SpringCloud Config 就是一个可以帮助你实现统一配置选择 ...

随机推荐

  1. Android -- 贝塞尔曲线公式的推导

    1,最近看了几个不错的自定义view,发现里面都会涉及到贝塞尔曲线知识,深刻的了解到贝塞尔曲线是进阶自定义view的一座大山,so,今天先和大家来了解了解. 2,贝塞尔曲线作用十分广泛,简单举几个的栗 ...

  2. PRINCE2的国际形势?光环国际项目管理培训

    PRINCE2的使用和应用非常广泛.在过去的12个月里,超过60,000人参加了PRINCE2基础资格(Foundation)或从业资格(Practitioner)考试.现在每周参加考试的人数超过了2 ...

  3. angular替代Jquery,常用方法支持

    1.angular.bind(self,fn.args);   切换作用域执行 2.angular.copy(source,[destination]);   拷贝和深度拷贝 3.angular.eq ...

  4. Vue.use自定义自己的全局组件

    通常我们在vue里面使用别人开发的组件,第一步就是install,第二步在main.js里面引入,第三步Vue.use这个组件.今天我简单的也来use一个自己的组件. 这里我用的webpack-sim ...

  5. PHP童鞋改JAVA代码怎么处理

    用线上升级平台代码练手,学习JAVA.飞哥建议我们自己从头再搭建一套,提高会大.我自己作为一个JAVA出身的人,用了几天时间学会PHP的经验来看.最好,先在原来代码基础上改些东西.熟悉了基本语法之后再 ...

  6. liunx文件与用户和群组

    文件基本属性 在图片中alogrithm的文件属性为drwxrwxr-x,其中d代表此文件为目录. 后面rwx,rwx,r-x分别代表文件所属者(ower),组(group),其他用户(other)的 ...

  7. 【WCF】错误处理(四):一刀切——IErrorHandler

    前面几篇烂文中所介绍到的错误方式,都是在操作协定的实现代码中抛出 FaultException 或者带泛型参数的detail方案,有些时候,错误的处理方法比较相似,可是要每个操作协定去处理,似乎也太麻 ...

  8. java 基础知识七 装箱和拆箱

    java  基础知识七  装箱和拆箱 数据类型可分为两大种,基本数据类型(值类型)和类类型(引用数据类型) 装箱:把基本类型用他们相对应的引用类型包装起来,使他们可以具有对象的特质    基本数据类型 ...

  9. app专项测试自动化测试方法思路与实现

    秉着个人意愿打算把python+rf接口自动进行彻底结束再做些其它方面的输出~但事与愿违,但领导目前注重先把专项测试方面完成,借此,先暂停python+rf(主要是与Jenkins集成+导入DB+微信 ...

  10. vscode奇淫记(上)

    每次换editor都是一种煎熬,从最早的eclipse,sublime,webstorm到现在在用的atom,换编辑器的驱动是寻找更酷炫和轻量的平衡点,其实我真的蛮喜欢atom的,酷炫!那我这次打算入 ...