单元测试误区

2021年09月15日 阅读数:3
这篇文章主要向大家介绍单元测试误区,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

单元测试布道之一:定义、分类与策略

 

 

 

在开始以前

即使从业若多年,不写单元测试的开发人员并很多见。关于单元测试的相关知识和实践网上连篇累牍,无须从零开始陈述,本系列预计三四章,本单为序,部份内容来自网上资料整理,后续内容添加自行编写的内容,出处见于文章末尾,请自行取用。html

什么是单元测试

关于测试概念很是多,在进行定义以前有必要先对测试进行分类,避免你们使用相同术语表达不一样的意思。node

测试的分类

软件测试从不一样的角度审视有着不一样的分类方式,好比按测试方法有“黑盒”、“白盒”之分,按按测试方向有功能、性能、安全、兼容性、稳定性测试。开发人员关注按阶段分类的测试,列举以下。git

  • 单元测试:检查代码判断是否有问题
  • 集成测试:测试模块和模块的链接有没有问题
  • 系统测试:测试软件的整个总体。功能,安全,性能等等测试
  • 验收测试:甲方或者客户来验收这个软件是否是它要的软件,协助验收

单元策略在开发阶段完成。面对繁多的分类方式,Google 有本身的命名:小型测试、中型测试和大型测试。github

能够看到 Google 所谓小型测试就是单元测试,咱们引入其定义。redis

单元测试的定义

单元测试是指对软件中的最小可测试单元进行检查和验证,是最低级别的测试活动。开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。一般而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。json

  • 验证代码与设计相符合;
  • 跟踪需求与设计的实现;
  • 发现设计和需求中存在的缺陷;
  • 发如今编码过程当中引入的错误。

单元测试与其余测试的区别

单元测试与集成测试的区别c#

  • 测试对象不一样:单元测试对象是实现了具体功能的程序单元;集成测试对象是概要设计规划中的模块及模块间的组合。
  • 测试方法不一样:单元测试中的主要方法是基于代码的白盒测试;集成测试中主要使用基于功能的黑盒测试。
  • 测试时间不一样:集成测试晚于单元测试。
  • 测试内容不一样:单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后可否达到预期效果。

单元测试与系统测试的区别缓存

  • 单元测试属于白盒测试,从开发者的角度出发,关注的是单元的具体实现、内部逻辑结构和数据流向;系统测试属于黑盒测试,从用户角度出发,证实系统已知足用户的须要。
  • 单元测试使问题及早暴露,便于定位解决,属于早期测试;系统测试是一种后期测试,定位错误比较困难。
  • 单元测试容许多个被测单元同时进行测试;系统测试时基于需求规格说明书。

单元测试的必要性

由于场景覆盖、逻辑不自闭甚至低级错误等诸多因素致使代码很难编写一次就正确执行,这就须要单元测试存在。而项目复杂度和代码量日益增加,手工回归测试时间愈来愈长,这就须要单元测试来兜底。安全

若是读者经历开发维护过没有单元测试的中型项目,很难赞成代码很难不变成臭不可闻的 shit mountain:不敢轻易重构,添加功能当心翼翼避免触碰到不知道在哪里的隐匿逻辑引入问题。而测试人员更是叫苦不迭,回归一轮下来时间久,线上问题层出不穷。bash

测试金字塔

一个健康、快速、可维护的测试组合应该是这样的:写许多小而快的单元测试,适当写一些更粗粒度的测试,写不多高层次的端到端测试。

大量单元测试做为金字塔基底,在此之上是一些集成测试,再往上是自动化相关测试。

  • 越靠近金字塔底部,测试组织起来越快,开展的成本越低
  • 越靠近金字塔顶部,测试组织起来越慢,开展的成本越高

来自微软的统计数据:bug 在单元测试阶段被发现,平均耗时3.25小时,若是漏到系统测试阶段,要花费11.5小时。

在开发阶段发现 bug,其解决成本远远比上线以后暴露问题要低得多得多。

代码的可测试性

截止目前为止咱们都在推广形而上学的内容,从如今开始,咱们以 dotnet 相关示例说明可测试性相关内容。要保证每一个组件的正确性以及能够校验变化,实际上是但愿将代码的质量保障提早,保证每一个组件在开发阶段可以测试;而想要每一个组件可以测试,在设计过程当中,就要保证每一个模块是能够测试的,而这就是可测试性。

单元测试不只用来测试代码功能,还能够用来测试代码设计,很差写单测的代码都是很差的代码。

好的测试容易写、可读、可靠、快速。咱们在设计以及编写代码时,必须将可测试性归入考量,在定义可测试性时不妨先看反模式

未决行为/非肯定性

// BAD
public class PowerTimer { public String GetMeridiem() { var time = DateTime.Now; if (time.Hour >= 0 && time.Hour < 12) { return "AM"; } return "PM"; } } public class PowerTimerTest { [Fact] public void get_meridiem_before_12_return_am() { // HOW? // Assert.Equal(new PowerTimer().GetMeridiem(), "AM"); } } 

DateTime.Now 本质上是一个隐藏的输入,在程序执行期间或测试运行之间可能会更改,对其调用将产生不一样的结果。引入方法参数能够修复该 API:

public class PowerTimer
{
	public String GetMeridiem(DateTime time) { if (time.Hour >= 0 && time.Hour < 12) { return "AM"; } return "PM"; } } public class PowerTimerTest { [Fact] public void get_meridiem_before_12_return_am() { var time = new DateTime(2021, 6, 15, 1, 0, 0); Assert.Equal(new PowerTimer().GetMeridiem(time), "AM"); } } 

直接依赖于实现

// BAD
public class DepartmentService { private CacheManager _cacheManager = new CacheManager(); public List<Department> GetDepartmentList() { List<Department> result; if (_cacheManager.TryGet("department-list", out result)) { return result; } // ... } } public class CacheManager { public bool TryGet<T>(string key, out T value) { // ... 

假设 CacheManager 直接去访问 redis 或 memcached 之类的缓存,就没办法进行单元测试了。解开紧密耦合的依赖,注入对象能修复该 API。

public class DepartmentService
{
	private CacheManager _cacheManager; public DepartmentService(CacheManager cacheManager) { _cacheManager = cacheManager; } public List<Department> GetDepartmentList() { List<Department> result; if (_cacheManager.TryGet("department-list", out result)) { return result; } // ... } } 

额外地说:"依赖注入" 是广义概念,并不限于 Microsoft.Extensions.DependencyInjection、Autofat、Castle 之类框架和其使用。

全局变量/单例模式

像 C# 等高级语言中没有全局变量,但单例模式是存在的,它是全局变量的另外一种形式。

// BAD
public class UserService { public User CreateUser(string name) { var id = GlobalCounter.Instance.NextId(); var user = new User(id, name); // ... } } public class GlobalCounter { private static readonly GlobalCounter Instance = new GlobalCounter(); public long NextId() { // ... 

单例模式一样依赖于真实的依赖关系,并在组件之间引入了没必要要的紧密耦合,但并非说不能使用单例模式。注入对象能修复该 API。

public class UserService
{
    private readonly GlobalCounter _globalCounter; public UserService(GlobalCounter globalCounter) { _globalCounter = globalCounter; } public User CreateUser(string name) { var id = _globalCounter.NextId(); var user = new User(id, name); // ... } } 

静态方法/函数

静态方法是不肯定性或反作用行为的另外一个潜在来源。它们能够轻松引入紧密耦合,并使咱们的代码不可测试。ASPNET MVC 中 HttpContext 是密封(sealed)类,彻底没有可测试性。微软前后引入了 HttpContextBase 及 HttpContextWrapper 来补救,并最终在 ASPNET Core 中将其抛弃。

// BAD
public void GetPageTitle() { if (HttpContext.Current.User.Identity.IsAuthenticated) { Page.Title = "Home page for " + HttpContext.User.Identity.Name; } else { Page.Title = "Home page for guest user."; } } 

固然并非说不能使用静态方法/函数,静态方法/函数不应依赖于外部环境,系统时间,网络等。

// BAD
public static bool CheckNodejsInstalled() { return Environment.GetEnvironmentVariable("PATH").Contains("nodejs", StringComparison.OrdinalIgnoreCase); } 

传递参数能修复该 API。

public static bool CheckNodejsInstalled(string path) { return path != null && path.Contains("nodejs", StringComparison.OrdinalIgnoreCase); } 

复杂继承

若是父类须要 mock 某个依赖才能进行单元测试,那其派生类在编写单元测试的时候,都要 mock 这个依赖对象。理论上层次越深 mock 工做越多,其实这也是高耦合的一种体现,使得很难编写单元测试。

abstract class Issue
{
	public Issue(Content content) { // do stuff with content } } class RegularIssue : Issue { public RegularIssue(Content content, Plan plan) : base(content) { // do stuff with plan } } class SignificantIssue : RegularIssue { public SignificantIssue(Content content, Plan plan, Bug bug) : base(content, plan) { // do stuff with bug } } 

也许咱们只想测试 SignificantIssue 的部分功能,可是构造其实例须要引入不相关的依赖,虽然你可能将其置空了事,但若是父类进行了很严谨的非空检查甚至是类型检查,测试恐怕并非那么容易。

高耦合代码

耦合度高的代码很难找到单元测试的切入点,也很难写出高效的测试代码。单元测试像是花盆里的沙子,保证可测试的过程要求咱们很好的拆分代码,它会下降土壤的粘度(耦合性)

私有方法

私有方法没法测试,若是但愿被测试则应考虑设计的合理性。对于 dotnet 项目来讲,InternalsVisibleTo 能够帮助咱们测试内部类,后文会略有篇幅描述。

单元测试策略

若是进行单元测试,这里推荐自底向上或孤立的单元测试策略。

  • 自底向上的单元测试:先对最底层的基本单元进行测试,模拟调用该单元的单元作驱动模块。而后再对上面一层进行测试,用下面已被测试过的单元作桩模块。依此类推,直到测试完全部单元。
  • 孤立单元测试:不考虑每一个单元与其它单元之间的关系,为每一个单元设计桩模块或驱动模块。每一个模块进行独立的单元测试。

这里引入了一些术语像"桩(stub)"之类,后文会有篇幅描述,能够先使用 mock/fake 做为替代理解。

Newtonsoft.Json 中最基本的对象是 JToken,其继承结构以下:

Newtonsoft.Json.Linq.JToken 
├── Newtonsoft.Json.Linq.JContainer 
│   ├── Newtonsoft.Json.Linq.JArray 
│   ├── Newtonsoft.Json.Linq.JConstructor 
│   ├── Newtonsoft.Json.Linq.JObject 
│   └── Newtonsoft.Json.Linq.JProperty 
└── Newtonsoft.Json.Linq.JValue 
    └── Newtonsoft.Json.Linq.JRaw 

UML 图更直观

该图做于 2013 年,仍适用于当前版本的 json.net,可见基设计之稳定。

Src/Newtonsoft.Json.Tests/Linq 中展现了相关的测试实现

  • JTokenTests.cs:最基本的测试,不依赖其余实现
  • JValueTests.cs:使用 JToken 测试 JValue 的方法,
  • JArrayTests.cs:使用 JValue 测试 JArray 的方法
  • JObjectTests.cs:使用 JValue 测试 JObject 的方法,少许使用 JProperty
  • JConstructorTests.cs:使用 JToken 和 JValue 测试 JConstructor 的方法
  • JRawTests.cs:使用 JToken 测试 JRaw 的方法

单元测试误区

现代开发框架作了不少工做使得组织项目变得容易,但不该深度开发框架中的高级特性。

若是脱离开发框架没法作单元测试,就说明代码已经不具有可测试性。大量借助开发框架进行单元测试,会蒙蔽团队的眼睛,让团队成员看不到代码边的有多糟糕。

部分参考

上一篇: Task异常