2013-08-18 31 views
3

目前我每个TestMethod内被测创建对象写我的单元测试。这样做的主要原因是促进自我遏制,便于可读性/调试,还可能在施工期间调整依赖关系。创建被测对象TestMethod的VS TestInitialize

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原则,或者让事情变得自成一体的可读性/逻辑的目的呢?

+1

我使用TestInitialize的方式,你给第二。正是出于这个原因。我相信如果您的单元测试课程集中并且只测试'一件事'(没有蔓延),那么这种方式可以使测试变得很好并且干净,并且'设置'会被排除在外。我不认为可读性会受到这种干扰。 – CodeBeard

回答

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 
    //... 
} 

注意如何提高可读性。除了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(); 

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

+1

我认为对于99%的案例,你对一个简单工厂的建议可以满足我的需求。在更复杂的结构组合中,我可以看到第二种解决方案的好处。感谢您的详细回复。 –