我正在尝试理解控制反转以及它如何帮助我进行单元测试。我已经阅读了几篇关于IOC的在线解释及其作用,但我只是不太了解它。
我开发了一个示例项目,其中包括使用StructureMap进行单元测试。 StructureMap设置代码如下:
private readonly IAccountRepository _accountRepository
public Logon()
{
_accountRepository = ObjectFactory.GetInstance<IAccountRepository>();
}
我不理解的是,就像我看到的那样,我可以简单地将上述声明如下:
AccountRepository _accountRepository = new AccountRepository();
它将与先前的代码做同样的事情。所以,我只是想知道是否有人能够以一种简单的方式向我解释,使用IOC的好处是什么(特别是在处理单元测试时)。
由于
答案 0 :(得分:2)
Inversion of Control是让框架回调用户代码的概念。这是一个非常抽象的概念,它只是描述了库和框架之间的区别,或者它可以被描述为“框架的定义特征”。我们的代码调用了一个库,而框架则控制着我们的代码。任何框架都提供了允许我们插入代码的钩子。
控制反转是一种模式,只有在构建框架时您是框架开发人员时,或者当您是应用程序开发人员与框架代码交互时才能应用。但是,在专门使用应用程序代码时,IoC不适用。
依赖于抽象而不是实现的行为称为依赖性反转,并且应用程序和框架开发人员都可以实现依赖性反转。所以你所说的IoC实际上就是依赖倒置,正如Krzysztof已经评论过:你所做的不是IoC。我将从现在开始讨论依赖倒置。
依赖倒置基本上有两种形式/实现:服务定位器和依赖注入。
使用服务定位器模式,可以从需要依赖关系的类中调用静态工厂。一般来说,它看起来像这样:
public class Service
{
public void SomeOperation() {
IDependency dependency =
ServiceLocator.GetInstance<IDependency>();
dependency.Execute();
}
}
这个示例对您来说应该很熟悉,因为您正在使用Logon
方法执行此操作:您正在使用服务定位器模式。
使用依赖注入模式,可以从外部注入类所需的所有依赖项;最好使用构造函数。类本身不负责获取其依赖项。这个责任在调用堆栈中向上移动。使用依赖注入时,前一个类看起来像这样:
public class Service
{
private readonly IDependency dependency;
public Service(IDependency dependency)
{ this.dependency =依赖; }
public void SomeOperation()
{ this.dependency.Execute(); } }
两种模式都是依赖性反转,因为在这两种情况下,Service
类都不负责创建依赖项,也不知道它正在使用哪种实现。它只是与界面交谈。这两种模式都使您可以灵活地使用类所使用的实现,从而允许您编写更灵活的软件。
然而,服务定位器模式存在许多问题,这就是为什么它被认为是反模式的原因。您已经遇到了这些问题,因为您想知道依赖性反转(在您的情况下服务定位器)如何帮助您进行单元测试。
答案是服务定位器模式对单元测试没有帮助。相反:它使单元测试非常困难。通过让类调用ObjectFactory
,您可以在两者之间创建一个硬依赖关系。替换IAccountRepository
进行测试也意味着您的单元测试必须使用ObjectFactory
。这使您的单元测试更难阅读。但更重要的是,由于ObjectFactory
是一个静态实例,所有单元测试都使用同一个实例,这使得很难在每个测试的基础上单独运行测试并交换实现。
我过去使用过服务定位器模式,我处理这个模式的方法是在服务定位器中注册依赖项,我可以逐个线程地更改(使用[ThreadStatic]字段下的盖子)。这允许我并行运行我的测试(默认情况下MSTest会做什么),同时保持测试被隔离。然而,问题在于,这种情况变得非常复杂,各种技术问题使测试变得混乱,这让我花了很多时间来解决这些技术问题,而我本可以写更多的测试。
这些问题的真正解决方案是依赖注入。一旦你通过构造函数注入一个类需要的依赖项,所有这些问题就会消失。这不仅非常清楚了解类所需的依赖项(没有隐藏的依赖项),而且每个单元测试本身都负责注入所需的依赖项。这使得编写测试变得更加容易,并且可以防止您在单元测试中配置DI容器。
答案 1 :(得分:1)
这背后的想法是让您可以替换默认的帐户存储库实现,以获得更多单元可测试版本。在单元测试中,您现在可以实例化一个不进行数据库调用的版本,而是返回固定数据。这样,您可以专注于测试方法中的逻辑,并释放对数据库的依赖性。
这在很多层面都比较好: 1)您的测试更稳定,因为您不再需要担心由于数据库中的数据更改而导致测试失败 2)由于您没有调用外部数据源,因此测试运行速度会更快 3)您可以更轻松地模拟所有测试条件,因为您的模拟存储库可以返回测试任何条件所需的任何类型的数据
答案 2 :(得分:0)
回答你问题的关键是可测试性,如果你想管理注入对象的生命周期,或者你想让IoC容器为你做这件事。
比方说,您正在编写一个使用您的存储库的类,并且您想要测试它。
如果您执行以下操作:
public class MyClass
{
public MyEntity GetEntityBy(long id)
{
AccountRepository _accountRepository = new AccountRepository();
return _accountRepository.GetEntityFromDatabaseBy(id);
}
}
当您尝试测试此方法时,您会发现存在许多并发症: 1.必须已经设置了数据库。 2.您的数据库需要具有您正在查找的实体的表。 3.您用于测试的ID必须存在,如果因任何原因删除它,那么您的自动化测试现在就会被破坏。
如果您有以下内容:
public interface IAccountRepository
{
AccountEntity GetAccountFromDatabase(long id);
}
public class AccountRepository : IAccountRepository
{
public AccountEntity GetAccountFromDatabase(long id)
{
//... some DB implementation here
}
}
public class MyClass
{
private readonly IAccountRepository _accountRepository;
public MyClass(IAccountRepository accountRepository)
{
_accountRepository = accountRepository;
}
public AccountEntity GetAccountEntityBy(long id)
{
return _accountRepository.GetAccountFromDatabase(id)
}
}
现在您可以在不需要数据库的情况下单独测试MyClass类。
这有什么好处?例如,您可以执行类似的操作(假设您使用的是Visual Studio,但同样的原则适用于NUnit):
[TestClass]
public class MyClassTests
{
[TestMethod]
public void ShouldCallAccountRepositoryToGetAccount()
{
FakeRepository fakeRepository = new FakeRepository();
MyClass myClass = new MyClass(fakeRepository);
long anyId = 1234;
Account account = myClass.GetAccountEntityBy(anyId);
Assert.IsTrue(fakeRepository.GetAccountFromDatabaseWasCalled);
Assert.IsNotNull(account);
}
}
public class FakeRepository : IAccountRepository
{
public bool GetAccountFromDatabaseWasCalled { get; private set; }
public Account GetAccountFromDatabase(long id)
{
GetAccountFromDatabaseWasCalled = true;
return new Account();
}
}
因此,您可以非常自信地测试MyClass类是否使用IAccountRepository实例从数据库获取Account实体,而无需拥有数据库。
你仍然可以在这里做一百万件事来改进这个例子。您可以使用像Rhino Mocks或Moq这样的Mocking框架来创建虚假对象,而不是像我在示例中那样自己编写它们。
通过这样做,MyClass类完全独立于AccountRepository,因此当loosley耦合概念发挥作用并且您的应用程序可测试且更易于维护时。
通过这个例子,您可以看到IoC本身的好处。现在,如果你不使用IoC容器,你必须实例化所有依赖项并在Composition Root中适当地注入它们,或者配置一个IoC容器,以便它可以为你完成。
问候。