其生命周期范围由IoC容器处理的单元测试对象

本文关键字:处理 单元测试 对象 IoC 生命 周期 范围 | 更新日期: 2023-09-27 18:10:25

我使用微软单元测试,并有以下内容:

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。

然而,由于简单注入器处理生命周期范围,我不能向简单注入器请求AccountCommandHandler对象,因为它会返回一个将CreateAccountCommandWasCalled设置为false的新对象。

对于单元测试来说,除了将CreateAccountCommandWasCalled设置为静态变量之外,还有什么更强大的测试方法?

其生命周期范围由IoC容器处理的单元测试对象

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

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

在您的情况下,我不会将IoC容器与生产配置一起用于设置单元测试。相反,我会切换到在测试中手动创建对象,并使用mock工具(如Moq)来控制依赖关系。

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

Mark Seemann (AutoFixture的开发人员)在几周前写了一篇关于使用AutoFixture和IoC作为自动模拟容器的博文。我建议使用类似这样的东西来构建单元测试。

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

您的测试项目应该为IoC配置拥有自己的Composition根。您可以配置IoC容器以返回AccountCommandHandlers的模拟对象。您的测试不是检查布尔成员,而是检查Handle(CreateCommand)是否至少被调用过一次。对于Moq,它看起来像这样:
mock.Verify(foo => foo.Handle(createAccountCommand), Times.AtLeastOnce());

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

对你的问题有一个更"哲学"的回答:-)

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

我的基本原理是,您需要您的测试对测试的上下文有完全的控制,而IOC可以拿走一些这种控制。在我看来,单元测试应该尽可能集中、小而可预测!

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

如果你的类需要有一个IOC容器的内部实例,把它从类中分离出来,变成某种类型的"控制器"。

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

这样,在您的测试"设置"中,您将在运行时实际地拔掉由IOC提供的"生命周期"。

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

如果需要的话,可以模拟IOC。

作为旁注,设计良好的IOC容器的好处之一是它应该使单元测试更容易——因为它应该阻止类依赖于类的实际"具体实例",并鼓励使用可互换的接口。

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

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

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

另一个注意事项:我并没有声称自己是单元测试专家!它们非常有用,但是比起编码的其他方面,我仍然更纠结于测试。

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

单元测试的艺术

你还应该问自己一个问题:我到底想测试什么?

你的测试套件不需要测试第三方库。在上面的代码中,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实例以对其进行断言。如果您信任库来完成它的工作,您只需新建处理程序并测试其内部逻辑。