其生命周期范围由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设置,并确保应用程序的不同部分能够协同工作。
但是,对于集成测试,您不应该使用模拟或存根对象。模拟和存根在单元测试中有它们的用途。单元测试就是测试代码中尽可能小的部分。在单元测试中,您可以使用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实例以对其进行断言。如果您信任库来完成它的工作,您只需新建处理程序并测试其内部逻辑。