在TestMethod与TestInitialize中创建测试中的对象

本文关键字:测试 对象 创建 TestInitialize TestMethod | 更新日期: 2023-09-27 18:28:00

目前我正在通过在每个TestMethod中创建被测对象来编写单元测试。这样做的主要原因是为了提高自包含性、可读性/调试性,并可能在构建过程中调整依赖关系。

public class MyClass
{
    public MyClass(ISomeService serviceA, ISomeOtherService serviceB)
    {
        ...
    }
}
[TestMethod]
public void it_should_do_something()
{
    var myClass = new MyClass(serviceA, serviceB);
    myClass.DoSomething();
    ...
}

然而,这正成为一场维护噩梦,尤其是当类上可能有很多测试,并且我想向构造函数添加另一个依赖项时。它要求我更改测试类中的每一行构造。

我希望通过在TestInitialize方法中初始化对象来保持干燥,这将改进维护,同时仍然保持自包含方面。

private MyClass _myClass;
[TestInitialize]
public void Init()
{
    _myClass = new MyClass(serviceA, serviceB);
}

这样做的唯一缺点是,如果我需要注入一组不同的依赖项,我可能需要重新初始化对象。对于第一次查看代码的另一个开发人员来说,读取/调试可能也不太清楚。这种方法还有其他我没有想到的负面影响吗?

  1. 这种情况下的最佳实践是什么?

  2. 在TestMethod之外初始化类是不是一种糟糕的做法?

  3. 我应该支持维护和DRY原则,还是为了可读性/逻辑目的而保持事物的独立性?

在TestMethod与TestInitialize中创建测试中的对象

测试应该:1)可读性,2)易于编写/维护。"可读"是什么意思?这意味着它最大限度地减少了在代码中跳转以了解特定代码行上正在执行的操作。

考虑在测试项目中创建一个工厂。我认为这是测试安排可读性最好的方法之一:

public static class MyClassFactory
{
  public static MyClass Create(ISomeService serviceA, ISomeOtherService serviceB)
  {
    return new MyClass(serviceA, serviceB);
  }
  public static MyClass CreateEmpty()
  {
    return new MyClass();
  }
//Just an example, but in general not really recommended.
//May be acceptable for simple projects, but for large ones
//remembering what does "Default" mean is an unnecessary burden
//and it somehow reduces readability.
  public static MyClass CreateWithDefaultServices()      
  {
    return new MyClass(/*...*/);
  }
  //...
}

(第二种选择将在后面解释)。然后你可以这样使用它:

[TestMethod]
public void it_should_do_something()
{
    //Arrange
    var myClass = MyClassFactory.Create(serviceA, serviceB);
    //Act
    myClass.DoSomething();
    //Assert
    //...
}

请注意它是如何提高可读性的。除了serviceAserviceB,我只留下它们来符合您的示例,您需要理解的一切都在这一行中。理想情况下,它看起来是这样的:

[TestMethod]
public void it_should_do_something()
{
    //Arrange
    var myClass = MyClassFactory.Create(
        MyServiceFactory.CreateStubWithTemperature(45),
        MyOtherServiceFactory.CreateStubWithContents("test string"));
    //Act
    myClass.DoSomething();
    //Assert
    //...
}

这提高了可读性。请注意,即使DRY的设置已经结束,所有的工厂方法都是简单而明显的。没有必要查阅它们的定义——它们第一眼就很清楚。

是否可以进一步提高书写和维护的易用性

一个很酷的想法是在MyClass的测试项目中提供扩展方法,以允许可读的配置:

public static MyClassTestExtensions
{
  public static MyClass WithService(
      this MyClass @this,
      ISomeService service)
  {
    @this.SomeService = service; //or something like this
    return @this;
  }
  public static MyClass WithOtherService(
      this MyClass @this,
      ISomeOtherService service)
  {
    @this.SomeOtherService = service; //or something like this
    return @this;
  }
}

然后你当然这样使用它:

[TestMethod]
public void it_should_do_something()
{
    //Arrange
    var serviceA = MyServiceFactory.CreateStubWithTemperature(45);
    var serviceB = MyOtherServiceFactory.CreateStubWithContents("test string");
    var myClass = MyClassFactory
        .CreateEmpty()
        .WithService(serviceA)
        .WithOtherService(serviceB);
    //Act
    myClass.DoSomething();
    //Assert
    //...
}

当然,服务不是这样配置的一个好例子(通常没有它们对象就不可用,因此没有提供默认的构造函数),但对于任何其他属性,它都给了你一个巨大的优势:有了N个属性来配置,你就可以保持一种干净可读的方式,用N个扩展方法创建具有所有可能配置的测试对象,而不是N x N个工厂方法,并且在构造函数或工厂方法调用中没有N长参数列表。

如果您的类不像上面假设的那样方便,一个可能的解决方案是将其子类化,并定义子类的工厂和扩展。

如果你的对象只存在其部分属性是没有意义的,并且它们是不可设置的,你可以流畅地配置一个工厂,然后创建对象:

//serviceA and serviceB creation
var myClass = MyClassFactory
  .WithServiceA(serviceA)
  .WithServiceB(serviceB)
  .CreateMyClass();

我希望这样的工厂的代码很容易推导,所以我不会在这里写它,因为这个答案看起来已经有点长了;)