结合单元测试(模拟)和依赖注入框架
可能的重复:
使用 IoC 进行单元测试
我认为我在理解单元测试和/或依赖注入正在工作。我使用 NUnit 和 Rhino Mocks 进行单元测试,并使用 Ninject 作为依赖注入框架。总的来说,我认为这两者是完美的——但不知何故,它似乎变得更加复杂和难以理解。
(我会尽力编一个好例子,以保持干净和简单。这是关于我的,骑自行车)。
1.) 没有 DI/单元测试:
如果不知道 DI 和单元测试,我的代码就会像这样 - 我会很高兴:
public class Person
{
public void Travel()
{
Bike bike = new Bike();
bike.Ride();
}
}
public class Bike
{
public void Ride()
{
Console.WriteLine("Riding a Bike");
}
}
要骑自行车,我只需要: new Person().Travel();
2.) 使用 DI:
我不想要那种紧密耦合,所以我需要一个接口和一个 NinjectModule!我会有一些开销,但是只要代码易于阅读和理解就可以了。我将只传递修改后的 Person 类的代码,Bike 类不变:
public class Person
{
IKernel kernel = new StandardKernel(new TransportationModule());
public void Travel()
{
ITransportation transportation = kernel.Get<ITransportation>();
transportation.Ride();
}
}
我仍然可以骑自行车: new Person().Travel();
3.)考虑单元测试(无 DI):
为了能够检查 Ride-Method 是否被正确调用,我需要一个模拟。据我所知,接口注入一般有两种方式:构造函数注入和Setter注入。我选择构造函数注入作为示例:
public class Person
{
ITransportation transportation;
public person(ITransportation transportation)
{
this.transportation = transportation;
}
public void Travel()
{
transportation.Ride();
}
}
这一次,我需要传递自行车:new Person(new Bike()).Travel();
4.) 使用 DI 并准备单元测试
3 中的类。考虑单元测试(没有 DI) 无需修改即可完成工作,但我需要调用 new Person(kernel.Get
。通过这种方式,感觉我失去了 DI 的好处——Person 类可以调用 Travel,而无需任何耦合,也不需要知道交通是什么类型。另外,我认为这种形式缺乏示例 2 的很多可读性。
是这样完成的吗?或者还有其他更优雅的方式来实现依赖注入和单元测试(和模拟)的可能性?
(现在看来,这个例子实在是太糟糕了——大家应该知道他此刻乘坐的是什么交通工具……)
Possible Duplicate:
Using IoC for Unit Testing
I think I do have a Problem understanding the way Unit Tests and/or Dependency Injection are working. I'm using NUnit and Rhino Mocks for Unit testing and Ninject as an Dependency Incection Framework. In general, I though those two would fit perfeclty - but somehow, it seems like it gets just more complicated and harder to understand.
(I'll try to make up a good example, to keep it clean and easy. It's about me, riding a bike).
1.) Without DI / Unit Tests:
Without knowing of DI and Unit Tests, my code would have looked like that - and I'd be happy:
public class Person
{
public void Travel()
{
Bike bike = new Bike();
bike.Ride();
}
}
public class Bike
{
public void Ride()
{
Console.WriteLine("Riding a Bike");
}
}
To ride my bike i would just need: new Person().Travel();
2.) With DI:
I don't want that tight coupling, so I need an Interface and a NinjectModule! I'd have some Overhead, but that will be fine, as long as the code is easy to read and understand. I'll just pass the code for the modified Person class, the Bike class is unchanged:
public class Person
{
IKernel kernel = new StandardKernel(new TransportationModule());
public void Travel()
{
ITransportation transportation = kernel.Get<ITransportation>();
transportation.Ride();
}
}
I could still ride my bike with just: new Person().Travel();
3.) Considering Unit-Testing (without DI):
To be able to check if the Ride-Method is called properly, I'll need a Mock. As far as I know, there are gernerally two ways to inject an Interface: Constructor Injection and Setter Injection. I choose Constructor Injection for my example:
public class Person
{
ITransportation transportation;
public person(ITransportation transportation)
{
this.transportation = transportation;
}
public void Travel()
{
transportation.Ride();
}
}
This time, i would neet to pass the bike: new Person(new Bike()).Travel();
4.) With DI and preparing for Unit-Tests
The class in 3. Considering Unit-Testing (without DI) would do the job without modification, but I would need to call new Person(kernel.Get<ITransportation>());
. Through that, it feels like I'm loosing the benefit from DI - the Person class could call Travel without any coupling and any need to know what kind of class the transportation is. Also, I think this form is lacking a lot of the readability of example 2.
Is this how it is done? Or are there other - more elegant ways achieving Dependency Injection and the possibility to unit test (and mock)?
(Looking back, it seems the example is really bad - everyone should know what kind of transportation device he is riding at the moment...)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
一般来说,我会尽量避免使用 IoC 容器进行单元测试 - 只需使用模拟和存根来传递依赖项。
您的问题始于场景 2:这不是 DI - 这是 服务定位器(反)模式。对于真正的依赖注入,您需要传入依赖项,最好是通过构造函数注入。
场景 3 看起来不错,这是 DI,通常也是您如何在隔离中测试您的类 - 传递您需要的依赖项。我很少发现需要使用完整的 DI 容器进行单元测试,因为每个被测试的类只有几个依赖项,每个依赖项都可以被存根或模拟来执行测试。
我什至认为,如果您需要一个 IoC 容器,那么您的测试可能不够细粒度,或者您有太多依赖项。在后一种情况下,一些重构可能是为了从您正在使用的两个或多个依赖项形成聚合类(当然,前提是存在任何语义连接)。这最终会将依赖项的数量降低到您满意的水平。这个最大数量每个人都不一样,我个人力求最多4个,至少一只手就能数出来,嘲讽也不是太大的负担。
反对在单元测试中使用 IoC 容器的最后一个也是关键的论点是行为测试:如果您不能完全控制依赖项,您如何确定被测类的行为符合您想要的方式?
可以说,您可以通过消除具有为某些操作设置标志的类型的所有依赖项来实现此目的,但这是一项艰巨的工作。使用 RhinoMocks 或 Moq 等模拟框架来验证是否使用您指定的参数调用某些方法要容易得多。为此,您需要模拟要验证调用的依赖项,IoC 容器在这里无法帮助您。
Generally I try to avoid using a IoC container for my unit testing - just use mocks and stubs to pass in the dependencies.
Your problem starts in scenario 2: This is not DI - this is the service locator (anti-)pattern. For real Dependency Injection you need to pass in your dependencies, preferably via constructor injection.
Scenario 3 is looking good, this is DI and generally also how you are enabled to test your classes in isolation - pass in the dependencies you need. I rarely find the need to use a full DI container for unit testing since each class under test only will have a few dependencies, each of which can either be stubbed or mocked to perform the test.
I even would argue that if you need a IoC container, your tests are probably not fine-grained enough or you have too many dependencies. In the later case some refactoring might be in order to form aggregate classes from two or more of the dependencies you are using (only if there is any semantic connection of course). This will eventually drop the number of dependencies to a level that you are comfortable with. That maximum number is different for each person, I personally strive to have 4 at most, at least I can count them on one hand and mocking is not too much of a burden.
A last and crucial argument against using an IoC container in unit testing is behavioral testing: How can you be sure the class under test behaves the way you want it to if you are not in full control of your dependencies?
Arguably you can achieve this by stubbing out all dependencies with types that set flags for certain actions, but this is a big effort. It is much, much easier to use a mocking framework like RhinoMocks or Moq to verify that certain methods were called with the arguments you specify. For that you need to mock the dependencies you want to verify calls on, an IoC container cannot help you here.
你对一些事情感到困惑。
实现 3 比 2 更好,因为您不需要在单元测试中设置 DI 框架。
因此,在测试第 3 点时,您将执行以下操作:
DI 框架仅在生产代码中构建对象树时才需要。在您的测试代码中,您可以完全控制要测试的内容。当对类进行单元测试时,您会模拟所有依赖项。
如果您还想做一些集成测试,您可以将真正的自行车传递给您的人员类并进行测试。
完全隔离地测试类的想法是您可以控制每个代码路径。您可以使依赖项返回正确或不正确的值,甚至可以让它抛出异常。如果一切正常,并且您仅通过单元测试就获得了良好的代码覆盖率,那么您只需要进行一些更大的测试即可确保您的 DI 连接正确。
编写可测试代码的关键是将对象创建与业务逻辑分开。
You are getting some things confused.
Implementation 3 is better then number 2, because you don't need to setup the DI framework in your unit tests.
So when testing number 3 you would do:
The DI framework is something that's only needed when constructing object trees in production code. In your test code you have total control of what you want to test. When Unit Testing a class, you mock out all dependencies.
If you also want to do some integration testing you would pass a real Bike to your person class and test it.
The idea of testing classes in total isolation is that you can control each and every code path. You can make the dependency return correct or incorrect values or you can even have it throw an exception. If everything is working and you have a nice code coverage just from your unit tests, you will only need a couple of bigger tests to make sure that your DI is wired correctly.
The key to writing testable code is to split object creation from business logic.
我的 2 美分...
虽然第 2 点是依赖倒置原则 (DIP) 的示例,但它使用服务位置模式,而不是依赖注入。
您的第 3 点说明了依赖注入,其中 IoC 容器会在构建 Person 期间将依赖项(ITransportation)注入到构造函数中。
在您的真实应用程序和单元测试中,您也希望使用 IoC 容器来构建 Person(即不要直接 new Person)。如果您的单元测试框架支持,请使用服务定位器模式(
kernel.Get();
)或 DI(例如 Setter)。然后,这将构建 Person 及其依赖项(即 ITransportation 的已配置具体类)并将其注入到 Person 中(显然,在单元测试中,您的 IoC 将为模拟/存根 ITransportation 配置)
最后,它是依赖项您想要 Mock,即 ITransportation,以便您可以测试 Person 的 Transport() 方法。
由于 Bike 没有依赖项,因此可以直接/独立地对其进行单元测试(除非将依赖项添加到 Bike,否则不需要模拟来测试 Bike.Ride())。
My 2 cents...
Although point 2 is an example of the Dependency Inversion Principle (DIP), it uses the Service Location Pattern, rather than Dependency Injection.
Your point 3 Illustrates Dependency Injection, where the IoC container would inject the dependency (ITransportation) into the constructor during build up of Person.
In your real app AND the unit test, you would want to use the IoC container to build Person as well (i.e. don't new Person directly). Either use the service locator pattern (
kernel.Get<Person>();
), or DI (e.g. Setter) if your Unit Test Framework supports this.This would then Build Up Person AND its Dependencies (viz the Configured concrete class for ITransportation) and Inject that into Person as well (obviously, in the unit test your IoC would be configured for the mocked / stubbed ITransportation)
Finally, it is the Dependencies that you would want to Mock, i.e. ITransportation, so that you can test the Transport() method of Person.
Since Bike has no dependencies, it can be Unit Tested directly / independently (You don't need a mock to test Bike.Ride() unless a dependency is added to Bike).