7

我正在使用 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作为单元测试的新手,除了设置为静态变量之外,还有什么更健壮的测试方法?

4

4 回答 4

6

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

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

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

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

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

于 2013-04-26T06:35:42.937 回答
5

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

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

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

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

于 2013-04-25T22:12:56.373 回答
4

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

您的测试套件不一定需要测试 3rd 方库。在上面,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,如果您信任该库来完成其工作,您只需新建处理程序并测试其内部逻辑。

于 2013-04-29T10:23:59.933 回答
4

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

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

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

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

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

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

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

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

如果甚至需要,您可以模拟国际奥委会。

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

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

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

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

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

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

单元测试的艺术

于 2013-04-25T08:46:59.727 回答