单元测试

290 阅读3分钟

什么是单元测试

单元测试(Unit Test)是指对程序中的最小的可测试单元进行检查,以验证一个明确的功能是否正确,程序是否按预期执行。

为什么要进行单元测试

  • 有助于回归测试

回归缺陷是在对应用程序进行更改时引入的缺陷。 测试人员通常不仅测试新功能,还要测试预先存在的功能,以验证先前实现的功能是否仍按预期运行。

使用单元测试,可在每次生成后,甚至在更改一行代码后重新运行整套测试。 让你确信新代码不会破坏现有功能。

  • 促进良好的设计

当代码紧密耦合时,可能难以进行单元测试。 如果不为编写的代码创建单元测试,则耦合可能不太明显。

为代码编写测试会自然地解耦代码,因为采用其他方法测试会更困难

  • 提供文档

如果你有一套命名正确的单元测试,每个测试应能够清楚地解释给定输入的预期输出。 此外,它应该能够验证其确实有效

  • 帮助理解代码

单元测试这么好?为啥推动不起来

  • 耗时间
  • 系统可测试性差
  • 认为测试应由专门的测试人员来做

单元测试步骤

待测试的方法

/// <summary>
/// 除法运算
/// </summary>
/// <param name="numerator">分子</param>
/// <param name="denominator">分母</param>
/// <returns></returns>
public int Division(int numerator, int denominator){
    return numerator / denominator;
}

测试用例

[Fact]
public void Division_WithNonZeroDenminator(){
    // 准备
    const int denominator = 2;
    const int numerator = 4;
    // 执行
    var result = Division(numerator, denominator);
    // 断言
    Assert.Equals(2, result);
}

单元测试框架

使用Fakes隔离测试代码

Fakes 有两种方式:

  • 存根将类替换为可实现同一接口的小型替代项。 若要使用存根,你在设计应用程序时必须让每个组件仅依赖接口,而不依赖其他组件。 (“组件”是指一个类或一起开发和更新的一组类,通常包含在一个程序集中。)
  • 填充码在运行时修改应用的编译代码,这样就可以运行测试提供的填充码代码,而不用执行指定的方法调用。 填充码可用于替换对无法修改的程序集(如 .NET 程序集)的调用。

考虑一下代码

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

如果进行单元测试,会随着时间的变化,而导致结果不同

方法一 修改代码,使用接口解耦

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

这样的话,我们就可以通过mock来用一个自己可控的实现来替代不可控的

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

好的单元测试需要注意的点

  • 快速
  • 原子性
  • 单一职责
    每个单元测试只做一件事
  • 独立
    每个单元测试应该是独立的,单元测试之间无相互依赖,结果与单元测试的执行顺序无关
  • 隔离外部调用
    不依赖文件系统,数据库,本地时间,网络,环境变量等任何外部因素。
  • 可重复
  • 直观明了的方法名
  • 全面但不要冗余

测试用例应该全面考虑:正常输入,边界输入,非法输入,白盒覆盖(准对代码逻辑结构来设计测试用例)

哪些代码需要单元测试

  • 帮助类
  • 业务逻辑
  • 当代码出现bug时,需要在修复bug的同时添加相应的单元测试

参考资料