1.为什么需要测试

之前看过的大部分技术文章中都有说到,不写测试的程序员不是好程序员。但现实往往是为了赶进度,迅速交付,都是堆功能,然后进行简单的测试就算完成了,这样做带来的后果是很严重的。由于做的很赶,会造成个把月后对自己的代码都会感到陌生,再根据新需求往上加东西的时候都不知道会不会对现有功能有什么影响,就会照成各种返工,人工多次测试。

1.1防止回归

通常在进行新功能/模块的开发或者是重构的时候,测试会进行回归测试原有的已存在的功能,以验证以前实现的功能是否仍能按预期运行。
使用单元测试,可在每次生成后,甚至在更改一行代码后重新运行整套测试, 从而可以很大程度减少回归缺陷。

1.2减少代码耦合

当代码紧密耦合或者一个方法过长的时候,编写单元测试会变得很困难。当不去做单元测试的时候,可能代码的耦合不会给人感觉那么明显。为代码编写测试会自然地解耦代码,变相提高代码质量和可维护性。

2.单元测试的定义

按照维基百科上的说法,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。在面向对象编程中,最小单元就是方法,包括基类、抽象类、或者派生类(子类)中的方法。 按照通俗的理解,一个单元测试判断某个特定场条件下某个特定方法的行为,如斐波那契数列算法,冒泡排序算法。

3. 基本原则和规范

3.1 3A原则

3A分别是”**Arrange****Act****Assert**“, 分别代表一个合格的单元测试方法的三个阶段

  • 事先的准备
  • 测试方法的实际调用
  • 针对返回值的断言

一个单元测试方法可读性是编写测试时最重要的方面之一。 在测试中分离这些操作会明确地突出显示调用代码所需的依赖项、调用代码的方式以及尝试断言的内容.
所以在进行单元测试的编写的时候, 请使用注释标记出3A的各个阶段的, 如下示例

  1. [Fact]
  2. public async Task VisitDataCompressExport_ShouldReturnEmptyResult_WhenFileTokenDoesNotExist()
  3. {
  4. // arrange
  5. var mockFiletokenStore = new Mock<IFileTokenStore>();
  6. mockFiletokenStore
  7. .Setup(it => it.Get(It.IsAny<string>()))
  8. .Returns(string.Empty);
  9. var controller = new StatController(
  10. mockFiletokenStore.Object,
  11. null);
  12. // act
  13. var actual = await controller.VisitDataCompressExport("faketoken");
  14. // assert
  15. Assert.IsType<EmptyResult>(actual);
  16. }

3.2 尽量避免直接测试私有方法

尽管私有方法可以通过反射进行直接测试,但是在大多数情况下,不需要直接测试私有的private方法, 而是通过测试公共public方法来验证私有的private方法。
可以这样认为:private方法永远不会孤立存在。更应该关心的是调用private方法的public方法的最终结果。

3.3 重构原则

如果一个类/方法,有很多的外部依赖,造成单元测试的编写困难。那么应该考虑当前的设计和依赖项是否合理。是否有部分可以存在解耦的可能性。选择性重构原有的方法,而不是硬着头皮写下去.

3.4 避免多个断言

如果一个测试方法存在多个断言,可能会出现某一个或几个断言失败导致整个方法失败。这样不能从根本上知道是了解测试失败的原因。
所以一般有两种解决方案

  • 拆分成多个测试方法
  • 使用参数化测试, 如下示例

    1. [Theory]
    2. [InlineData(null)]
    3. [InlineData("a")]
    4. public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input)
    5. {
    6. // arrange
    7. var stringCalculator = new StringCalculator();
    8. // act
    9. Action actual = () => stringCalculator.Add(input);
    10. // assert
    11. Assert.Throws<ArgumentException>(actual);
    12. }

    当然如果是对对象进行断言, 可能会对对象的多个属性都有断言。此为例外。

3.5 文件和方法命名规范

文件名规范

一般有两种。比如针对UserController下方法的单元测试应该统一放在UserControllerTest或者UserController_Test下

单元测试方法名

单元测试的方法名应该具有可读性,让整个测试方法在不需要注释说明的情况下可以被读懂。格式应该类似遵守如下

  1. <被测试方法全名>_<期望的结果>_<给予的条件>
  2. // 例子
  3. [Fact]
  4. public void Add_InputNullOrAlphabetic_ThrowsArgumentException()
  5. {
  6. ...
  7. }

4.xunit

4.1基本使用

最基本的测试,使用 Fact标记测试方法,使用 Assert来断言自己对结果的预期。
可以使用 Theory来自己指定一批数据来进行测试,来实现测试数据驱动测试,简单的数据可以通过 InlineData直接指定,也可以使用 MemberData来指定一个方法来返回用于测试的数据,也可以自定义一个继承于 DataAttributeData Provider
新建一个类库ClassLibrary
image.png
新建类StringExtensions

  1. namespace System
  2. {
  3. public static class StringExtensions
  4. {
  5. public static bool IsNullOrWhiteSpace(this string str)
  6. {
  7. return string.IsNullOrWhiteSpace(str);
  8. }
  9. }
  10. }

新建xUnit测试项目,沿用上面的命名规范ClassLibraryTest,引用ClassLibrary项目
image.png
新建类StringExtensions_Test
添加IsNullOrWhiteSpace_Test方法,并用**[Fact]**特性标注为这是已知或被证明是真实的。
添加IsNullOrWhiteSpace_Test2方法,并用**[Theory]**特性标注为这是理论上成立的。使用**[InlineData]**注入测试数据。

  1. using System;
  2. using Xunit;
  3. namespace ClassLibraryTest
  4. {
  5. public class StringExtensions_Test
  6. {
  7. [Fact]
  8. public void IsNullOrWhiteSpace_Test()
  9. {
  10. Assert.True(string.Empty.IsNullOrWhiteSpace());
  11. }
  12. [Theory]
  13. [InlineData("")]
  14. [InlineData(" ")]
  15. public void IsNullOrWhiteSpace_Test2(string str)
  16. {
  17. Assert.True(str.IsNullOrWhiteSpace());
  18. }
  19. }
  20. }

运行测试
image.png
测试结果
image.png

5.引用

浅谈.Net Core后端单元测试
.NET 项目中的单元测试
xunit