.NET单元测试-入门

基于状态测试

  在上一篇文章中,我们举了一个带返回值的例子,那么无返回值的情况下又该怎样写单元测试呢?

有如下代码:

1
2
3
4
5
6
public IList<string> Names = new List<string>();
public void Reset()
{
Names.Clear();
}

我们发现,Reset方法内部执行的是Names列表的清空操作,这个操作可以抽象成对被测试类状态的更改,要验证状态更改是否符合预期,我们只需要验证更改前后是否符合预期即可。在这里,只需要测试Reset方法是否按照我们预期的把Names清空即可。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// 条件:Names不为空
/// 预期:清空Names
/// </summary>
[TestMethod()]
public void ResetTest_NamesNotEmpty_NamesEmpty()
{
//Arrange
var document = new Document();
document.Names.Add("name0");
document.Names.Add("name1");
//Action
document.Reset();
//Assert
Assert.AreEqual(document.Names.Count, 0);
}

依赖外部对象的测试

  单元测试需要能够快速独立运行,隔离掉对外部的依赖是非常必要的,比如文件系统、硬件数据、web服务等。

如下代码:

1
2
3
4
5
6
7
8
9
10
11
///<summary>
/// 判断当前字符串是否是合法的html字符串
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public bool IsValidHtml(string input)
{
var textService = new TextService();
return textService.IsValidHtml(input);
}

可以看到,当前方法依赖TextService来验证html,但是在运行单元测试时,TextService的状态是未知的,它甚至可能还未开发完成。因此,需要隔离掉对TextService的依赖。

而TextService是在IsValidHtml方法内部创建的,我们无法隔离,这个时候就需要对方法进行一系列的修改,以使得它达到可测试的要求(这就是所谓的单元测试约束设计)。

再进一步的分析,可以发现依赖的是TextService提供的IsValidHtml()方法,而并非TextService这个对象,这就好说了,让IsValidHtml()依赖可以提供html验证的接口,我们就可以不用依赖TextService这个对象了,我们抽取接口:

1
2
3
4
public interface ITextService
{
bool IsValidHtml(string input);
}

这样我们就可以从对具体实现的依赖解耦为对接口的依赖,因此,在测试方法中我们可以很方便的用一个假的ITextService的实现来替代真实的TextService,由此隔离对真实外部服务的依赖。

这个假的ITextService的实现我们称为 伪对象

如下SubTextService就是我们的伪对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SubTextService : ITextService
{
private bool _isValidHtml;
public void SetIsValidHtml(bool value)
{
_isValidHtml = value;
}
public bool IsValidHtml(string input)
{
return _isValidHtml;
}
}

有了伪对象,怎么使用起来呢?

接下来介绍几种伪对象注入的方式

  • 构造函数注入

    这种方式需要被测试类提供一个带有ITextService参数的构造函数,我们修改被测试类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public Document(ITextService textService)
    {
    _textService = textService;
    }
    /// <summary>
    /// 判断当前字符串是否是合法的html字符串
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    public bool IsValidHtml(string input)
    {
    return _textService.IsValidHtml(input);
    }

    接下来,在测试方法中就可以将伪对象注入进去了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /// <summary>
    /// 条件:传入Empty的字符串
    /// 预期:返回False
    /// </summary>
    [TestMethod()]
    public void IsValidHtml_EmptyInput_ReturnFalse()
    {
    //Arrange
    var subTextService = new SubTextService();
    subTextService.SetIsValidHtml(false);
    var document = new Document(subTextService);
    //Action
    var result = document.IsValidHtml(string.Empty);
    //Assert
    Assert.IsFalse(result);
    }

    这种方法比较简单,被测试类的代码改动也不大。
    但是,如果方法中依赖多个外部接口,需要构造函数的参数列表可能很长;或者被测试类中不同方法依赖了不同的外部接口,那么需要增加多个构造函数。
    因此,此方法需要根据情况谨慎使用。

  • 属性注入

    这种方式指的是被测试类将外部接口的依赖设计成可以公开属性:

    1
    public ITextService TextService { get; set; }

    这样在单元测试中就可以方便的将伪对象注入进去。
    这种方法简单,对被测试类改动小。
    但是,将TextService设计成属性,会给外部一种TextService的赋值非必需的误解,然而在我们的设计中TextService是必须的。
    因此,不推荐使用。

  • 工厂注入

    工厂注入指的是当我们依赖的第三方接口是用工厂新建时,通过给工厂中注入伪对象来隔离对真实对象的依赖。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static class TextServiceFactory
    {
    private static ITextService _textService = new TextService();
    public static ITextService Create()
    {
    return _textService;
    }
    public static void SetTextService(ITextService textService)
    {
    _textService = textService;
    }
    }

    这种方法也比较简单,需要对工厂方法进行修改,改动量也不大。
    可根据情况使用。

  • 派生类注入

    派生类注入指的是在设计的时候,把对外部的依赖对象的获取设计成可以被继承,这样伪对象就可以在不修改原来代码的情况下完成注入:

    1
    2
    3
    4
    protected virtual ITextService GetTextService()
    {
    return new TextService();
    }

    写单元测试的时候,只需要用伪对象继承被测试类,就可以在重写GetTextService时,注入伪对象。

    1
    2
    3
    4
    5
    6
    7
    8
    //Document为被测试类
    public class SubDocument : Document
    {
    protected override ITextService GetTextService()
    {
    return new SubTextService();
    }
    }

    在单元测试时,就直接使用SubDocument即可.
    这种方法比较简单,而且不需要修改被测试类代码。
    推荐此方法。

  写单元测试可以为我们的代码增加一层保护,在设计程序时考虑单元测试也可以优化我们的设计,好处多多,何乐而不为呢(●’◡’●)

-------------本文结束 感谢您的阅读-------------

本文标题:.NET单元测试-入门

文章作者:nero

发布时间:2017年03月17日 - 23:03

最后更新:2017年10月30日 - 08:10

原始链接:http://erdao123.oschina.io/nero/2017/03/17/UnitTest/NET单元测试-入门/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。