Reputation: 786
I'm trying to understand Inversion of Control and how it helps me with my unit testing. I've read several online explanations of IOC and what it does, but I'm just not quite understanding it.
I developed a sample project, which included using StructureMap for unit testing. StructureMap setup code like the following:
private readonly IAccountRepository _accountRepository
public Logon()
{
_accountRepository = ObjectFactory.GetInstance<IAccountRepository>();
}
The thing I'm not understanding though, is as I see it, I could simply declare the above as the following:
AccountRepository _accountRepository = new AccountRepository();
And it would do the same thing as the prior code. So, I was just wondering if someone can help explain to me in a simple way, what the benefit of using IOC is (especially when dealing with unit testing).
Thanks
Upvotes: 4
Views: 2584
Reputation: 172646
Inversion of Control is the concept of letting a framework call back into user code. It's a very abstract concept but in essence describes the difference between a library and a framework. IoC can be seen as the "defining characteristic of a framework." We, as program developers, call into libraries, but frameworks instead call into our code; the framework is in control, which is why we say the control is inverted. Any framework supplies hooks that allow us to plug in our code.
Inversion of Control is a pattern that can only be applied by framework developers, or perhaps when you're an application developer interacting with framework code. IoC does not apply when working with application code exclusively, though.
The act of depending on abstractions instead of implementations is called Dependency Inversion, and Dependency Inversion can be practiced by both application and framework developers. What you refer to as IoC is actually Dependency Inversion, and as Krzysztof already commented: what you're doing is not IoC. I'll discuss Dependency Inversion for the remainder of my answer.
There are basically two forms of Dependency Inversion:
Let's start with the Service Locator pattern.
A Service Locator supplies application components outside the [startup path of your application] with access to an unbounded set of dependencies. As its most implemented, the Service Locator is a Static Factory that can be configured with concrete services before the first consumer begins to use it. (But you’ll equally also find abstract Service Locators.) [source]
Here's an example of a static Service Locator:
public class Service
{
public void SomeOperation()
{
IDependency dependency =
ServiceLocator.GetInstance<IDependency>();
dependency.Execute();
}
}
This example should look familiar to you, because this what you're doing in your Logon
method: You are using the Service Locator pattern.
We say that a Service Locator supplies access to an unbounded set of dependencies, because the caller can pass in any type it wishes at runtime. This is opposite to the Dependency Injection pattern.
With the Dependency Injection pattern (DI), you statically declaring a class's required dependencies; typically, by defining them in the constructor. The dependencies are made part of the class's signature. The class itself isn't responsible for getting its dependencies; that responsibility is moved up up the call stack. When refactoring the previous Service
class with DI, it would likely become the following:
public class Service
{
private readonly IDependency dependency;
public Service(IDependency dependency)
{
this.dependency = dependency;
}
public void SomeOperation()
{
this.dependency.Execute();
}
}
Both patterns are Dependency Inversion, since in both cases the Service
class isn't responsible of creating the dependencies and doesn't know which implementation it is using. It just talks to an abstraction. Both patterns give you flexibility over the implementations a class is using and thus allow you to write more flexible software.
There are, however, many problems with the Service Locator pattern, and that's why it is considered an anti-pattern. You are already experiencing these problems, as you are wondering how Service Locator in your case helps you with unit testing.
The answer is that the Service Locator pattern does not help with unit testing. On the contrary: it makes unit testing harder compared to DI. By letting the class call the ObjectFactory
(which is your Service Locator), you create a hard dependency between the two. Replacing IAccountRepository
for testing, also means that your unit test must make use of the ObjectFactory
. This makes your unit tests harder to read. But more importantly, since the ObjectFactory
is a static instance, all unit tests make use of that same instance, which makes it hard to run tests in isolation and swap implementations on a per-test basis.
I used to use a static Service Locator pattern in the past, and the way I dealt with this was by registering dependencies in a Service Locator that I could change on a thread-by-thread basis (using [ThreadStatic]
field under the covers). This allowed me to run my tests in parallel (what MSTest does by default) while keeping tests isolated. The problem with this, unfortunately, was that it got complicated really fast, it cluttered the tests with all kind of technical stuff, and it made me spent a lot of time solving these technical problems, while I could have been writing more tests instead.
But even if you use a hybrid solution where you inject an abstract IObjectFactory
(an abstract Service Locator) into the constructor of Logon
, testing is still more difficult compared to DI because of the implicit relationship between Logon
and its dependencies; a test can't immediately see what dependencies are required. On top of that, besides supplying the required dependencies, each test must now supply a correctly configured ObjectFactory
to the class.
The real solution to the problems that Service Locator causes is DI. Once you statically declare a class's dependencies in the constructor and inject them from the outside, all those issues are gone. Not only does this make it very clear what dependencies a class needs (no hidden dependencies), but every unit test is itself responsible for injecting the dependencies it needs. This makes writing tests much easier and prevents you from ever having to configure a DI Container in your unit tests.
Upvotes: 2
Reputation: 6597
The key to answer your question is testability and if you want to manage the lifetime of the injected objects or if you are going to let the IoC container do it for you.
Let's say for example that you are writing a class that uses your repository and you want to test it.
If you do something like the following:
public class MyClass
{
public MyEntity GetEntityBy(long id)
{
AccountRepository _accountRepository = new AccountRepository();
return _accountRepository.GetEntityFromDatabaseBy(id);
}
}
When you try to test this method you will find that there are a lot of complications: 1. There must be a database already set up. 2. Your database needs to have the table that has the entity you're looking for. 3. The id that you are using for your test must exist, if you delete it for whatever reason then your automated test is now broken.
If instead you have something like the following:
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)
}
}
Now that you have that you can test the MyClass class in isolation without the need for a database to be in place.
How is this beneficial? For example you could do something like this (assuming you are using Visual Studio, but the same principles apply to NUnit for example):
[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();
}
}
So, as you can see you are able, very confidently, to test that the MyClass class uses an IAccountRepository instance to get an Account entity from a database without the need to have a database in place.
There are a million things you can still do here to improve the example. You could use a Mocking framework like Rhino Mocks or Moq to create your fake objects instead of coding them yourself like I did in the example.
By doing this the MyClass class is completely independent of the AccountRepository so that's when the loosley coupled concept comes into play and your application is testable and more maintainable.
With this example you can see the benefits of IoC in itself. Now if you DO NOT use an IoC container you do have to instantiate all the dependencies and inject them appropriately in a Composition Root or configure an IoC container so it can do it for you.
Regards.
Upvotes: 0
Reputation: 39248
The idea behind this is to enable you to swap out the default account repository implementation for a more unit testable version. In your unit tests you can now instantiate a version that doesn't make a database call, but instead returns back fixed data. This way you can focus on testing the logic in your methods and free yourself of the dependency to the database.
This is better on many levels: 1) Your tests are more stable since you no longer have to worry about tests failing due to data changes in the database 2) Your tests will run faster since you don't call out to an external data source 3) You can more easily simulate all your test conditions since your mocked repository can return any type of data needed to test any condition
Upvotes: 1