【问题标题】:How are you able to Unit Test your controllers without an IoC container?在没有 IoC 容器的情况下如何对控制器进行单元测试?
【发布时间】:2010-11-05 17:29:55
【问题描述】:

在构建我最新的 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 容器,而且我很可能很快就会走上这条路。但是,我很好奇不使用它们的人有什么替代方案。

【问题讨论】:

    标签: asp.net-mvc unit-testing inversion-of-control controller ioc-container


    【解决方案1】:

    是不是可以保持字段的直接实例化,同时提供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的可见性设置为package-private,并保持测试类的包相同,以便它可以调用setter。

    【讨论】:

    • @Peter,非常好的建议。您是否有在项目中使用 IoC 容器的经验,或者您还没有发现需求?
    • 我没有太多 .NET 经验,我的大部分工作是使用 Spring for IoC 处理 Java。它对于提高模块化非常有用。
    • @Peter:这几乎就是我使用的模式。我发布了一个做同样事情的答案,但使用了一些 C# 语言特性,作为 Java 人,你可能不知道。
    【解决方案2】:

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

    类似...

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

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

    【讨论】:

      【解决方案3】:

      一种选择是使用假货。

      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

      【讨论】:

        【解决方案4】:

        我正在扩展彼得的答案。

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

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

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

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-05-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-06-14
          • 2011-04-27
          相关资源
          最近更新 更多