目前,我正在通过在每个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
方法初始化对象来保持DRY,这样可以改善维护,同时保持自我控制方面。
private MyClass _myClass;
[TestInitialize]
public void Init()
{
_myClass = new MyClass(serviceA, serviceB);
}
我可以看到这样做的唯一缺点是,如果我需要注入一组不同的依赖项,我可能需要重新初始化对象。对于第一次查看代码的另一个开发人员来说,读取/调试也可能稍微不那么清楚。这种方法还有其他任何负面影响,我没有想到吗?
此方案的最佳做法是什么?
在TestMethod之外初始化类是不好的做法吗?
我是否应该支持维护和DRY原则,或者为了可读性/逻辑目的保持自包含?
答案 0 :(得分:7)
测试应该是: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
//...
}
请注意它如何提高可读性。在serviceA
和serviceB
旁边,我只是为了符合你的例子,你需要理解的一切都在这一行。理想情况下,它看起来像这样:
[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();
我希望这个工厂的代码很容易推断,所以我不会在这里写,因为这个答案看起来有点长;)