单元测试时我应该使用模拟对象吗?

时间:2010-11-05 14:52:20

标签: c# asp.net-mvc unit-testing tdd

在我的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保持我的服务类单元测试是否更好,但更改我的控制器单元测试以使用模拟服务对象?

<小时/> 修改: 在重新审视我的结构后,我想起了一个复杂的事情,它阻止了控制器单元测试中的模拟,因为我忘了我的结构比我原先说的要复杂一点。

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

控制器传递IRepositoryFactoryIRepositoryFactory是一个类,它可以使创建存储库变得容易,而无需将存储库直接存入控制器,或让控制器担心哪些服务类需要哪些存储库。我有一个InMemoryRepositoryFactory,它提供服务类InMemoryRepository<Entity>实例化,同样的想法适用于我的EFRepositoryFactory

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

所以例如

public class DocumentController : Controller
{
    private DocumentService _documentService;

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

我无法看到如何使用此架构模拟我的服务层,以便我的控制器经过单元测试而不进行集成测试。我可能有一个糟糕的单元测试架构,但我不知道如何更好地解决让我想要首先建立存储库工厂的问题。

3 个答案:

答案 0 :(得分: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,详细讨论重组代码的好处。)

答案 1 :(得分:2)

答案 2 :(得分:1)

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