.NET单元测试-入门
基于状态测试
在上一篇文章中,我们举了一个带返回值的例子,那么无返回值的情况下又该怎样写单元测试呢?
有如下代码:
1 public IList<string> Names = new List<string>(); 2 3 public void Reset() 4 { 5 Names.Clear(); 6 }
我们发现,Reset方法内部执行的是Names列表的清空操作,也就是对被测试类某一状态的更改,我们只需要测试Reset方法是否按照我们预期的把Names清空即可。如下:
/// <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服务等。
如下代码:
<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方法,我们抽取接口:
public interface ITextService { bool IsValidHtml(string input); }
这样我们就可以从对具体实现的依赖解耦为对接口的依赖,因此,在测试方法中我们就可以用一个假的ITextService的实现来替代真实的TextService,由此隔离对真实外部服务的依赖。
这个假的ITextService的实现我们称为 伪对象。
如下SubTextService就是我们的伪对象:
public class SubTextService : ITextService { private bool _isValidHtml; public void SetIsValidHtml(bool value) { _isValidHtml = value; } public bool IsValidHtml(string input) { return _isValidHtml; } }
有了伪对象,怎么使用起来呢?
接下来介绍几种伪对象注入的方式
-
构造函数注入
这种方式需要被测试类提供一个带有ITextService参数的构造函数,修改被测试类:
public Document(ITextService textService) { _textService = textService; } /// <summary> /// 判断当前字符串是否是合法的html字符串 /// </summary> /// <param name="input"></param> /// <returns></returns> public bool IsValidHtml(string input) { return _textService.IsValidHtml(input); }
由此,在测试方法中就可以将伪对象注入进去了:
/// <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); }
这种方法比较简单,被测试类的代码改动也不大。
但是,如果方法中依赖多个外部接口,需要的构造函数的参数列表可能很长;或者被测试类中不同方法依赖了不同的外部接口,那么需要增加多个构造函数。
因此,需要根据情况使用。
-
属性注入
这种方式指的是被测试类将外部接口的依赖设计成可以公开属性:
public ITextService TextService { get; set; }
这样在单元测试中就可以方便的将伪对象注入进去。
这种方法简单,被测试类改动小。
但是,将TextService设计成属性,会给外部一种TextService是可选的误解,然而在我们的设计中TextService并不是可选的。
因此,不推荐使用。
-
工厂注入
工厂注入指的是当我们依赖的第三方接口是用工厂新建时,通过给工厂中注入伪对象来隔离对真实对象的依赖。
public static class TextServiceFactory { private static ITextService _textService = new TextService(); public static ITextService Create() { return _textService; } public static void SetTextService(ITextService textService) { _textService = textService; } }
这种方法也比较简单,需要对工厂方法进行修改,改动量也不大。
因此,根据情况使用。
-
派生类注入
派生类注入指的是在设计的时候,把对外部的依赖对象的获取设计成可以被继承,这样伪对象就可以在不修改原来代码的情况下完成注入:
protected virtual ITextService GetTextService() { return new TextService(); }
写单元测试的时候,只需要用伪对象继承被测试类,就可以在重写GetTextService时,注入伪对象。
//Document为被测试类 public class SubDocument : Document { protected override ITextService GetTextService() { return new SubTextService(); } }
在单元测试时,就直接使用SubDocument即可.
这种方法比较简单,而且不需要修改被测试类代码。
因此,推荐次方法。
写单元测试可以为我们的代码增加一层保护,在设计程序时考虑单元测试也可以优化我们的设计,好处多多,何乐而不为呢(●'◡'●)