如何在没有IoC容器的情况下对控制器进行单元测试?

6

我正在构建最新的ASP.NET MVC项目,开始学习单元测试、依赖注入等技术。

现在我想对我的控制器进行单元测试,但是如果没有IoC容器,我很难找到适当的方法来实现这一点。

以一个简单的控制器为例:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository _repository = new SqlQuestionsRepository();

    // ... Continue with various controller actions
}

这个类由于直接实例化SqlQuestionsRepository而不是很适合进行单元测试。因此,让我们采用依赖注入的方式:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository _repository;

    public QuestionsController(IQuestionsRepository repository)
    {
        _repository = repository;
    }
}

这看起来更好了。我现在可以很容易地使用模拟的IQuestionsRepository编写单元测试了。然而,现在是谁来实例化控制器呢?在调用链的更高层,SqlQuestionRepository将必须被实例化。似乎我只是把问题转移到了其他地方,而没有摆脱它。
现在,我知道这是IoC容器可以帮助我为控制器连接依赖项,同时保持控制器易于单元测试的很好的例子。
我的问题是,如果没有IoC容器,该如何对这种性质的东西进行单元测试?
注意:我并不反对IoC容器,并且很快就会采用它们。 但是,我想知道不使用它们的人的替代方法。
4个回答

2

是否有可能保留字段的直接实例化并提供setter方法?在这种情况下,您只需要在单元测试期间调用setter方法。像这样:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository _repository = new SqlQuestionsRepository();

    // Really only called during unit testing...
    public QuestionsController(IQuestionsRepository repository)
    {
        _repository = repository;
    }
}

我不太熟悉.NET,但在Java中,这是一种改进现有代码以提高可测试性的常见方法。也就是说,如果您已经使用某些类并需要修改它们以提高代码覆盖率而不会破坏现有功能。

我们的团队以前做过这个,在通常情况下,我们将setter的可见性设置为包私有,并保持测试类的包相同,以便可以调用该setter。


@Peter,非常好的建议。您在项目中使用过IoC容器吗?或者您还没有发现需要使用它们? - mmcdole
我没有太多的.NET经验,我的大部分工作都是使用Spring进行IoC的Java开发。这对于提高模块化非常有用。 - Peter
@Peter:这基本上是我使用的模式。我发布了一个做同样事情的答案,但使用了一些C#语言特性,你作为Java开发者可能不知道。 - Seth Petry-Johnson

2
你可以在控制器中定义一个默认构造函数,它将具有某种默认行为。例如...
public QuestionsController()
    : this(new QuestionsRepository())
{
}

默认情况下,当控制器工厂创建控制器的新实例时,它将使用默认构造函数的行为。然后在您的单元测试中,您可以使用模拟框架将模拟对象传递到另一个构造函数中。


1

一种选择是使用伪造对象。

public class FakeQuestionsRepository : IQuestionsRepository {
    public FakeQuestionsRepository() { } //simple constructor
    //implement the interface, without going to the database
}

[TestFixture] public class QuestionsControllerTest {
    [Test] public void should_be_able_to_instantiate_the_controller() {
        //setup the scenario
        var repository = new FakeQuestionsRepository();
        var controller = new QuestionsController(repository);
        //assert some things on the controller
    }
}

另一个选择是使用模拟对象和模拟框架,可以动态生成这些模拟对象。
[TestFixture] public class QuestionsControllerTest {
    [Test] public void should_be_able_to_instantiate_the_controller() {
        //setup the scenario
        var repositoryMock = new Moq.Mock<IQuestionsRepository>();
        repositoryMock
            .SetupGet(o => o.FirstQuestion)
            .Returns(new Question { X = 10 });
        //repositoryMock.Object is of type IQuestionsRepository:
        var controller = new QuestionsController(repositoryMock.Object);
        //assert some things on the controller
    }
}

关于所有对象的构造位置。在单元测试中,你只需要设置一组最小的对象:一个正在被测试的真实对象和一些伪造或模拟的依赖项,这些依赖项是真实测试对象所需的。例如,正在测试的真实对象是 QuestionsController 的一个实例——它依赖于 IQuestionsRepository,因此我们可以给它一个假的 IQuestionsRepository,如第一个示例,或者像第二个示例中那样给它一个模拟的 IQuestionsRepository
然而,在真实系统中,你需要在软件的顶层设置整个容器。例如,在 Web 应用程序中,你需要在 GlobalApplication.Application_Start 中设置容器,将所有接口和实现类连接起来。

0

我在Peter的回答上进行了一些扩展。

在具有许多实体类型的应用程序中,控制器需要引用多个存储库、服务等是很常见的。我发现在我的测试代码中手动传递所有这些依赖关系非常繁琐(特别是因为给定的测试可能只涉及其中一两个)。在这种情况下,我更喜欢使用setter注入式IOC而不是构造函数注入。我使用的模式是:

public class QuestionsController : ControllerBase
{
    private IQuestionsRepository Repository 
    {
        get { return _repo ?? (_repo = IoC.GetInstance<IQuestionsRepository>()); }
        set { _repo = value; }
    }
    private IQuestionsRepository _repo;

    // Don't need anything fancy in the ctor
    public QuestionsController()
    {
    }
}

使用您特定的IOC框架语法替换IoC.GetInstance<>

在生产中,没有任何东西会调用属性设置器,因此第一次调用getter时,控制器将调用您的IOC框架,获取一个实例并存储它。

在测试中,只需在调用任何控制器方法之前调用setter即可:

var controller = new QuestionsController { 
    Repository = MakeANewMockHoweverYouNormallyDo(...); 
}

这种方法的好处,我个人认为:

  1. 在生产中仍然可以利用IOC。
  2. 在测试期间更容易手动构建控制器。您只需要初始化测试实际使用的依赖项。
  3. 如果您不想手动配置常见依赖项,则可以创建特定于测试的IOC配置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接