鼎鼎知识库
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

01测试开始篇.md 8.7KB

.NET中的测试

测试类型

  • 单元测试(Unit tests):测试单个组件或方法,与数据库、文件、外界交互无关
  • 集成测试(Integration tests):测试多个组件或方法,与数据库、文件、外界交互有关
  • 压力测试(Load tests):测试系统对压力的承受程度,比如并发用户、并发请求等

测试框架

  • xUnit:来自NUnit v2的作者,包括了最新的测试技术和思想。属于.NET基金会下的项目。
  • NUnit:从JUnit演变而来,最新的版本增加了很多新功能。属于.NET基金会下的项目。
  • MSTest:微软的测试框架,支持在.NET CLIVisual Studio中的扩展

.NET CLI

.NET CLI集成了IDE下的大部分功能,通过使用dotnet test命令运行。可以被用到持续集成交付管道中,也可以被用在测试自动化任务中。

IDE

相比.NET CLI有更多的功能,比如Live Unit TestingIDE独有的测试功能。

单元测试的最佳实践

为什么需要单元测试

  • 相比功能测试更少的时间:功能测试需要一个懂领域知识的人,测试时间较长。单元测试不需要对被测试系统有太多了解,在极短的时间内完成测试。
  • 能更好地应对变化:只要有代码的更改,无论是新功能,还是老功能,跑一遍单元测试就可以做到
  • 测试的文档化:单元测试本身说明了给定输入下的输出
  • 反向促使代码解耦:耦合性很强的代码很难测试

好单元测试的特点

  • 快:毫秒级
  • 隔离的:不依赖于任何文件或数据库,可以单独跑通
  • 可重复的:如果在测试测时候没有改变条件,测试结果是一样的
  • 自检的:在没有人员介入的情况下判定测试成功或失败
  • 用时较少的:用时比写代码更少,如果发现写测试的时间较多,说明哪个地方有问题

测试覆盖率Code coverage

测试覆盖率不见得越大越好,只是表明有多少代码被测试了,需要在实际项目中把握测试覆盖率的度。

理解Fake, Mock, Stub

  • Fake:不确定是Mock还是Stub的时候用Fake
  • Mock:直接影响到测试结果就用Mock
  • Stub: 只是某个依赖的替代,不直接影响到测试结果

不好的

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);//测试结果与MockOrder没有直接关系

好的

var stubOrder = new FakeOrder();//使用Fake可以转换成Mock或Stub
var purchase = new Purchase(stubOrder);
purchase.ValdiateOrders();
Assert.True(purchase.CanBeShipped);//测试结果与stubOrder没有关系

好的

var mockOrder = new FakeOrder();//使用Fake可以转换成Mock或Stub
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Valdiated);//测试结果与mockOrder有直接关系

命名。好的命名说明了测试意图。通常是:被测方法名称_场景或条件_期待结果

不好的

[Fact]
public void Test_Single(){}

好的

[Fact]
public void Add_SingleNumber_ReturnsSameNumber(){}

遵循AAA原则

不好的

[Fact]
public void Add_EmptyString_ReturnsZero()
{
	//Arrange
	var stringCalculator = new StringCalculator();
	
	//Assert
	Assert.Equal(0, stringCalculator.Add(""));
}

好的

[Fact]
public void Add_EmptyString_ReturnsZero()
{
	//Arrange
	var stringCalculator = new StringCalculator();
	
	//Act
	var actual = stringCalculator.Add("");
	
	//Assert
	Assert.Equal(0, actual);
}

保持输入简单原则

不好的

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

好的

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

避免使用系统保留字

不好的

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

好的

[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
    var stringCalculator = new StringCalculator();
    const string MAXIMUM_RESULT = "1001";//赋值给常量

    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

    Assert.Throws<OverflowException>(actual);
}

避免逻辑

不好的

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }
}

好的

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

帮助方法替代setupteadown

不好的

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

好的

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}

private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

避免一个测试方法中出现多个推断

不好的

[Fact]
public void Add_EdgeCases_ThrowsArgumentExceptions()
{
    Assert.Throws<ArgumentException>(() => stringCalculator.Add(null));
    Assert.Throws<ArgumentException>(() => stringCalculator.Add("a"));
}

好的

[Theory]
[InlineData(null)]
[InlineData("a")]
public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input)
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add(input);

    Assert.Throws<ArgumentException>(actual);
}

测试公共方法而不是私有方法

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

[Fact]
public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

使用接口获取静态值

有一个和日期相关的方法

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

不好的

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

不管哪一天,总一个通不过。

好的。提炼出一个获取日期的接口。

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}
public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}
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);
}