【问题标题】:Unit testing object whose lifetime scope is handled by IoC container生命周期范围由 IoC 容器处理的单元测试对象
【发布时间】:2013-04-22 17:16:52
【问题描述】:

我正在使用 Microsoft 单元测试并具有以下内容:

public class AccountCommandHandlers :
    Handler<CreateAccountCommand>,
     Handler<CloseAccountCommand>
{
    public bool CreateAccountCommandWasCalled = false;
    public bool CloseAccountCommandWasCalled = false;

    public void Handle(CreateAccountCommand command)
    {
        CreateAccountCommandWasCalled = true;
    }

    public void Handle(CloseAccountCommand command)
    {
        CloseAccountCommandWasCalled = true;
    }
}

[TestMethod]
public void CanRaiseInternalHandlers()
{
    var iocContainer = SimpleInjectorWiringForMembus.Instance;
    iocContainer.Bootstrap(
        AppDomain.CurrentDomain.GetAssemblies());

    var membus = MembusWiring.Instance;
    membus.Bootstrap();

    membus.Bus.Publish(new CreateAccountCommand() { Id = 100 });
    membus.Bus.Publish(new CloseAccountCommand() { Id = 100 });
}

我正在使用一个处理对象生命周期范围的 IoC 容器(简单注入器)。 Membus 将命令连接到命令处理程序,并通过 IoC 容器进行解析。

上述代码运行正常,命令处理程序将其局部变量设置为 true。

但是,由于 Simple Injector 处理生命周期范围,我不能向 Simple Injector 请求 AccountCommandHandler 对象,因为它会返回一个新对象,并将 CreateAccountCommandWasCalled 设置为 false。

对于单元测试不熟悉,除了将CreateAccountCommandWasCalled 设置为静态变量之外,还有什么更稳健的测试方法?

【问题讨论】:

    标签: c# unit-testing dependency-injection simple-injector membus


    【解决方案1】:

    正如其他人已经提到的,您实际上是在运行集成测试。然而,这不是问题。集成测试适合测试您的 IoC 设置并确保应用程序的不同部分协同工作。

    但是,对于集成测试,您不应该使用模拟或存根对象。模拟和存根在单元测试中有它们的用途。单元测试就是测试代码中尽可能小的部分。在单元测试中,您使用模拟来控制您的类所具有的所有依赖项的行为。一年前我写了一封blog,介绍了集成测试和单元测试之间的区别以及如何在测试中使用模拟。

    在您的情况下,我不会使用具有生产配置的 IoC 容器来设置您的单元测试。相反,我会切换到在测试中手动创建对象,并使用像 Moq 这样的模拟工具来控制依赖关系。

    但这也是可以自动化的。一个很棒的工具是AutoFixture。 “夹具”是指您运行测试所需的基线。这可能是一些示例数据、您需要的模拟和存根以及其他设置代码。

    Mark Seemann(AutoFixture 背后的开发人员)几周前写了一篇不错的博客,介绍了将 AutoFixture 与 IoC 一起用作Auto-mocking Container。我建议使用这样的东西来构建你的单元测试。

    【讨论】:

      【解决方案2】:

      正如 Steven 在他的 cmets 中所说,听起来您正在编写一个集成测试,在这种情况下使用 IoC 容器确实有意义。

      您的测试项目应该有自己的 IoC 配置组合根。您可以配置您的 IoC 容器以返回 AccountCommandHandlers 的模拟对象。您的测试,而不是检查布尔成员,可以改为检查 Handle(CreateCommand) 是否被调用了至少一次。使用起订量,它看起来像这样:

      mock.Verify(foo => foo.Handle(createAccountCommand), Times.AtLeastOnce());
      

      如果由于某种原因您不能在 IoC 配置中使用模拟框架,那么为这个测试用例创建自己的模拟类会很容易。

      【讨论】:

        【解决方案3】:

        这是对您问题的更“哲学”的答案:-)

        如果可能的话,我的建议是不要在测试中使用 IOC 容器!

        我的理由是,您需要您的测试完全控制测试的上下文,而 IOC 可以取消部分控制。 IMO,单元测试应该尽可能集中、小和可预测!

        考虑将模拟对象发送到您的测试类,而不是实际类。

        如果您的类需要一个 IOC 容器的内部实例,请将其从类中分解到某种“控制器”中。

        您可以通过多种方式完成此操作,我最喜欢使用像 Rhino Mocks 这样的框架。

        这样,您实际上会在测试“设置”中将 IOC 在运行时提供的“生命周期”存根。

        所以测试应该完全控制(通过模拟和存根)何时创建或销毁对象,使用像 Rhino 这样的框架。

        如果需要,您可以模拟 IOC。

        附带说明一下,设计良好的 IOC 容器的好处之一是它应该使单元测试更容易 - 因为它应该阻止类依赖于类的实际“具体实例”,并鼓励使用可互换的接口而是。

        您应该尝试在运行时依赖 IOC 容器,该容器提供您设计的接口的具体实现。

        请注意,弄清您实际测试的内容通常也很重要。单元测试通常应该专注于测试单个类上单个方法的行为。

        如果您实际上在一个类上测试“更多”而不是仅一种方法,例如一个类如何与其他类交互,这意味着您很可能正在编写“集成”测试,而不是真正的“单元”测试。

        另一个注意事项:我并没有声称自己是单元测试方面的专家!它们非常有用,但与编码的几乎任何其他方面相比,我仍然在努力进行测试。

        为了进一步阅读,我强烈推荐 Roy Osherove 的“单元测试的艺术”。还有其他的。

        The Art of Unit Testing

        【讨论】:

        • 好吧,这个答案更像是对单元测试的大吐槽,而不是对您非常具体的问题的回答,抱歉!希望有一天有人发现它有用:)
        • 我同意你在这里所说的,但我感觉OP实际上正在编写一个集成测试来验证系统是否可以正常工作。在这少数情况下,您实际上需要测试包括 IoC 容器和 Membus 在内的基础设施。您可能应该进行一些这样的测试,但是拥有这些测试很重要。
        【解决方案4】:

        您还应该问自己一个问题:我真正想测试什么?

        您的测试套件不一定需要测试第 3 方库。在上面,membus 被设计为将消息传递给处理程序,该库中的测试确保是这种情况。

        如果你真的想测试消息 -> IOC 处理程序委托,在 Membus 的情况下,你可以编写一个总线 -> IOC 适配器进行测试:

        public class TestAdapter : IocAdapter
        {
            private readonly object[] _handlers;
        
            public TestAdapter(params object[] handlers)
            {
                _handlers = handlers;
            }
        
            public IEnumerable<object> GetAllInstances(Type desiredType)
            {
                return _handlers;
            }
        }
        

        然后将它与被测实例一起使用。

                var bus =
                    BusSetup.StartWith<Conservative>()
                            .Apply<IoCSupport>(
                                ioc => ioc.SetAdapter(new TestAdapter(new Handler<XYZ>()))
                            .SetHandlerInterface(typeof (IHandler<>)))
                            .Construct();
        

        您将在其中保留 Handler 实例以对其进行断言。 OTOH,如果您信任该库来完成其工作,您只需新建处理程序并测试其内部逻辑。

        【讨论】:

        • 好点,但是还有一些其他的东西(序列化)用于其他测试(我没有包括在内,以免使事情复杂化),membus 实际上只是被列为不作为瘦身的神器下测试。顺便说一句,membus 很棒 :)
        猜你喜欢
        • 2012-11-06
        • 1970-01-01
        • 1970-01-01
        • 2015-04-29
        • 1970-01-01
        • 2011-04-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多