2010-11-05 108 views
2

在我的ASP.Net MVC应用程序中,我使用IoC来简化单元测试。我的应用程序的结构是Controller -> Service Class -> Repository类型的结构。为了做单元测试,我有一个InMemoryRepository类继承我的IRepository,而不是去数据库,它使用内部List<T>成员。当我构建我的单元测试时,我只传递一个内部存储库的实例,而不是我的EF存储库。单元测试时我应该使用模拟对象吗?

我的服务类通过我的存储库类实现的AsQueryable接口从存储库检索对象,因此允许我在没有服务类的情况下在我的服务类中使用Linq,同时仍然抽象出数据访问层。在实践中,这似乎运作良好。

我看到的问题是,每当我看到单元测试谈到,他们都使用模拟对象,而不是我看到的内部方法。在面值上它是有道理的,因为如果我的InMemoryRepository失败,不仅我的InMemoryRepository单元测试失败,但是这个失败也会级联到我的服务类和控制器中。更现实的是,我更关心影响控制器单元测试的服务类中的故障。

我的方法还要求我为每个单元测试做更多的设置,并且随着事情变得更加复杂(例如,我在服务类中实现授权),设置变得更加复杂,因为我必须确保每个单元测试会正确授予它与服务类别,因此该单元测试的主要方面不会失败。我可以清楚地看到模拟对象如何在这方面提供帮助。

但是,我不明白如何解决这个完全与嘲笑,仍然有效的测试。例如,我的一个单元测试是,如果我调用_service.GetDocumentById(5),它会从存储库中获取正确的文档。这是一个有效的单元测试的唯一方法(据我了解)是如果我有2或3个文件存储,并且我的GetdocumentById()方法正确地检索Id为5的那个。

我将如何有一个嘲笑库AsQueryable调用,以及如何确保我没有掩盖任何问题,我用我的Linq语句硬编码设置模拟存储库时的返回语句?使用InMemoryRepository保持我的服务类单元测试更好吗,但将控制器单元测试更改为使用模拟服务对象?


编辑: 去在我的结构之后再次我记得是防止控制器单元测试嘲讽的并发症,因为我忘了我的结构比我原来说有点复杂。

A Repository是一种对象类型的数据存储,所以如果我的文档服务类需要文档实体,它会创建一个IRepository<Document>

控制器通过IRepositoryFactoryIRepositoryFactory是一个类,它可以很容易地创建存储库,而无需将存储库直接存入控制器,或让控制器担心哪些服务类需要哪些存储库。我有一个InMemoryRepositoryFactory,它给出了服务类别InMemoryRepository<Entity>实例化,并且我的EFRepositoryFactory也有同样的想法。

在控制器的构造函数中,通过传入传入该控制器的IRepositoryFactory对象来实例化私有服务类对象。

因此,例如

public class DocumentController : Controller 
{ 
    private DocumentService _documentService; 

    public DocumentController(IRepositoryFactory factory) 
    { 
     _documentService = new DocumentService(factory); 
    } 
    ... 
} 

我不能看到如何使用这种架构嘲笑我的服务层,使我的控制器单元测试,不融合测试。我可能有一个糟糕的单元测试架构,但我不确定如何更好地解决让我想要首先创建一个存储库工厂的问题。

+2

“使用InMemoryRepository'保持我的服务类单元测试更好,但更改我的控制器单元测试以使用模拟服务对象?”对,就是这样。 – 2010-11-05 14:56:22

+0

我刚刚更新了这个问题,在测试控制器对我来说看起来不太明显的时候,为什么嘲笑服务层更加详细。 – KallDrexx 2010-11-05 15:24:13

回答

3

一个解决问题的方法是改变你的控制器的需求,而不是构建服务本身IDocumentService实例:

public class DocumentController : Controller 
{ 
    private IDocumentService _documentService; 

    // The controller doesn't construct the service itself 
    public DocumentController(IDocumentService documentService) 
    { 
     _documentService = documentService; 
    } 
    ... 
} 

在您的实际应用,让你的IoC容器注入IRepositoryFactory实例为您服务。在您的控制器单元测试中,只需根据需要嘲笑服务。

(而看到Misko Hevry's article about constructors doing real work为重组你的代码这样的好处的进一步讨论。)

+0

谢谢。这很有意义,您提供的链接也是如此! – KallDrexx 2010-11-05 18:10:55

1

我认为你的方法对于测试服务层本身是合理的,但是,正如你所建议的那样,如果服务层完全嘲笑你的业务逻辑和其他高级测试会更好。这使得您的高级测试更易于实施/维护,因为如果测试已经过测试,则无需再次执行服务层。

2

就个人而言,我会设计围绕工作模式的引用库该股系统。这可以使事情变得更简单,并允许您以原子方式运行更复杂的操作。您通常会有一个IUnitOfWorkFactory作为服务类中的依赖项提供。服务类将创建一个新的工作单元,并且该工作单元引用存储库。你可以看到这样一个例子:here

如果我理解正确,您会关注一个(低级别)代码中的错误,导致很难查看实际问题。你拿InMemoryRepository作为一个具体的例子。

虽然你的关注是有效的,但我个人不会担心失败InMemoryRepository。它是一个测试对象,您应该尽可能简化这些测试对象。这可以防止您必须为测试对象编写测试。大多数情况下,我认为他们是正确的(但是,我有时通过编写Assert语句在这样的类中使用自检)。当这样的对象出现故障时,测试将失败。这不是最佳的,但你通常会很快地发现问题在我的经验中。要提高生产力,您必须在某处绘制一条线。

服务器引起的控制器错误是另一杯茶IMO。虽然你可以嘲笑这项服务,但这会使测试变得更加困难和不太可靠。最好不要测试服务。只测试控制器!控制器会调用服务,如果你的服务表现不好,你的控制器测试会发现。这样你只能测试应用程序中的顶级对象。代码覆盖范围将帮助您找到您未测试的部分代码。当然,这在所有场景中都是不可能的,但这通常运作良好。当服务与模拟存储库(或工作单元)一起工作时,这将工作得很好。


你的第二个问题是那些depedencies让你有多少测试设置。对此我有两件事要说。

首先,我尝试我的依赖倒置减少到只有我需要能够运行我的单元测试。调用系统时钟,数据库,Smtp服务器和文件系统应该是伪造的,以使单元测试快速可靠。其他事情我尽量不要反转,因为你越嘲笑,测试变得越不可靠。你正在测试更少。最小化的依赖反转(你有什么需要有良好的RTM单元测试)有助于使测试设置更容易。

但(第二点),你还需要编写单元测试的方式,他们的可读性和可维护性(单元测试最困难的部分,其实还是制作软件一般)。当一个类获得新的依赖关系时,具有较大的测试设置会让他们难以理解,并使得测试代码难以改变。我发现的让测试更具可读性和可维护性的最佳方法之一是使用简单的工厂方法在测试类集中的,你在测试(我从来没有使用嘲讽框架)需要类型的创建。我使用了两种模式。一个是一个简单的工厂方法,如一个,创建一个有效的类型:

FakeDocumentService CreateValidService() 
{ 
    return CreateValidService(CreateInitializedContext()); 
} 

FakeDocumentService CreateValidService(InMemoryUnitOfWork context) 
{ 
    return new FakeDocumentSerice(context); 
} 

这样的测试可以简单地调用这些方法时,他们需要一个有效的对象,他们只需调用的工厂方法之一。当然,当这些方法之一意外地创建了一个无效的对象时,许多测试都会失败。这很难防止,但很容易修复。容易修复意味着测试是可维护的。

我用另一种方式是使用保存的参数/你要创建的实际对象的属性的容器类型。当对象具有许多不同的属性和/或构造函数参数时,这会特别有用。有一个工厂的容器和对象来创建一个构建器方法混合本,你会得到非常可读的测试代码:

[TestMethod] 
public void Operation_WithValidArguments_Succeeds() 
{ 
    // Arrange 
    var validArgs = CreateValidArgs(); 

    var service = BuildNewService(validArgs); 

    // Act 
    service.Operation(); 
} 

[TestMethod] 
[ExpectedException(typeof(InvalidOperationException))] 
public void Operation_NegativeAge_ThrowsException() 
{ 
    // Arrange 
    var invalidArgs = CreateValidArgs(); 

    invalidArgs.Age = -1; 

    var service = BuildNewService(invalidArgs); 

    // Act 
    service.Operation(); 
} 

这样就可以让测试只能指定哪些事项!这对于使测试可读是非常重要的! CreateValidArgs()方法可以创建一个具有超过100个参数的容器,这些参数将会生成一个有效的SUT(被测系统)。你现在集中在一个地方默认的有效配置。我希望这是有道理的。


你的第三个问题约为未能测试,如果LINQ查询与给定的LINQ提供程序的行为果然。这是一个有效的问题,因为编写LINQ(对于表达式树)查询非常容易,这些查询在通过内存对象使用时可以完美运行,但在查询数据库时会失败。有时候,翻译查询是不可能的(因为你调用了一个没有对应数据库的.NET方法),或者LINQ提供者有限制(或者错误)。特别是实体框架3.5的LINQ提供者很难。

但是,这是你无法定义每个单元测试解决问题。因为当你在测试中调用数据库时,它不再是单元测试。然而,单元测试从来没有完全取代手动测试:-)

不过,这是一个值得关注的问题。除了单元测试外,您还可以进行集成测试。在这种情况下,您可以使用真正的提供程序和(专用)测试数据库运行代码。在数据库事务中运行每个测试,并在测试结束时回滚事务(TransactionScope适用于此!)。但是请注意,编写可维护的集成测试比编写可维护的单元测试更困难。你必须确保你的测试数据库的模型是同步的。每个集成测试都应该在数据库中插入该测试所需的数据,这通常需要编写和维护很多工作。最好的做法是尽量减少集成测试的数量。有足够的集成测试让您对系统进行更改有信心。例如,必须在单个测试中调用带有复杂LINQ语句的服务方法,通常足以测试您的LINQ提供程序是否能够构建有效的SQL。大多数时候我只是假设LINQ提供者将具有与LINQ to Objects(.AsQueryable())提供者相同的行为。再一次,你将不得不在某处画线。

我希望这会有所帮助。

相关问题