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

4 回答 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 的旁注,这是重构现有代码以提高可测试性的常用方法。IE,如果您有已经在使用的类并且需要修改它们以便在不破坏现有功能的情况下提高代码覆盖率。

我们团队之前也这样做过,通常我们将 setter 的可见性设置为 package-private 并保持测试类的包相同,以便它可以调用 setter。

于 2009-06-25T02:40:25.777 回答
2

您的控制器可以有一个默认构造函数,该构造函数将具有某种默认行为。

就像是...

public QuestionsController()
    : this(new QuestionsRepository())
{
}

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

于 2009-06-25T02:41:10.520 回答
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.

于 2009-06-25T02:45:52.397 回答
0

我正在扩展彼得的答案。

在具有大量实体类型的应用程序中,控制器需要引用多个存储库、服务等并不少见。我发现在我的测试代码中手动传递所有这些依赖项很乏味(特别是因为给定的测试可能只涉及其中的一两个)。在这些情况下,我更喜欢 setter-injection 风格的 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.GetInstance<>为您的特定 IOC 框架使用的任何语法。

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

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

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

这种方法的好处,恕我直言:

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