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

时间:2017-05-25 11:58:47   收藏:0   阅读:6378

注:本文示例环境

为什么要编写单元测试

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

框架选型

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

基本概念

好的测试

基础实践

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

例:

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使用的测试:

例:

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方法进行测试

例:

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) 断言 (来源)

Moq(4.7.10) It参数约束

相关资料

相关推荐

demo

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!