DotNETWeekly-io / DotNetWeekly

DotNet weekly newsletter
MIT License
198 stars 3 forks source link

【文章推荐】不使用 Interface 而完成单元测试的方法 #655

Closed gaufung closed 1 month ago

gaufung commented 1 month ago

https://www.code4it.dev/blog/unit-tests-without-interfaces/

gaufung commented 1 month ago

image

对于单元测试,通常的做法是将一些外部依赖抽象成接口,这样可以通过接口不同的实现来进行单元测试。但是 C# 除了接口,还可以通过其他方式完成单元测试:

  1. Virtual

C# 中 Virtual 关键字用来表示该属性或者方法可以在子类中进行重载,所以在单元测试中可以构造新的子类来重写部分方法。

public class NumbersRepository
{
    private readonly int[] _allNumbers;
    public NumbersRepository(){
        _allNumbers = Enumerable.Range(0, 100).ToArray();
    }
    public virtual IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50);
}

public class NumbersSearchService
{
    private readonly NumbersRepository _repository;
    public NumbersSearchService(NumbersRepository repository) {
        _repository = repository;
    }
    public bool Contains(int number){
        var numbers = GetNumbers();
        return numbers.Contains(number);
    }
    public IEnumerable<int> GetNumbers() => _repository.GetNumbers();
}

// 单元测试
internal class StubNumberRepo : NumbersRepository
{
    private IEnumerable<int> _numbers;
    public void SetNumbers(params int[] numbers) => _numbers = numbers;
    public override IEnumerable<int> GetNumbers() => _numbers;
}

[TestMethod] 
public void Should_WorkWithStubRepo() {
  // Arrange
  var repository = new StubNumberRepo();
  repository.SetNumbers(1, 2, 3);
  var service = new NumbersSearchService(repository);
  // Act
  var result = service.Contains(3);
  // Assert
  Assert.AreEqual(result, true);
}
  1. New

C# 中 new 关键字也能隐藏父类的方法和属性,这样我们只需要测试其他内容就可以完成单元测试

public class NumbersSearchService {
  private readonly NumbersRepository _repository;
  public NumbersSearchService(NumbersRepository repository) {
    _repository = repository;
  }
  public bool Contains(int number) {
    var numbers = GetNumbers();
    return numbers.Contains(number);
  }
  public IEnumerable<int> GetNumbers() => _repository.GetNumbers();
}

internal class StubNumberSearch : NumbersSearchService {
  private IEnumerable<int> _numbers;
  private bool _useStubNumbers;

  public void SetNumbers(params int[] numbers) {
    _numbers = numbers.ToArray();
    _useStubNumbers = true;
  }

  public new IEnumerable<int> GetNumbers() => _useStubNumbers ?
      _numbers:base.GetNumbers();
}

在单元你测试中只需要测试 StubNumberSearch 即可。

  1. Moq

Moq.NET 社区广泛使用的单元测试框架,使用 Moq 可以很方便地构造测试子类

public class NumbersRepository {
  private readonly int[] _allNumbers;

  public NumbersRepository() {
    _allNumbers = Enumerable.Range(0, 100).ToArray();
  }

  public virtual IEnumerable<int> GetNumbers() => Random.Shared.GetItems(
      _allNumbers, 50);
}

[TestMethod]
public void Should_WorkWithMockRepo() {
  // Arrange
  var repository = new Moq.Mock<NumbersRepository>();
  repository.Setup(_ => _.GetNumbers()).Returns(new int[]{1, 2, 3});
  var service = new NumbersSearchService(repository.Object);

  // Act
  var result = service.Contains(3);

  // Assert
  Assert.AreEqual(result, true);
}