Introduction
Test Driven Development is great if you know how to do it right. Unfortunately, many of the tutorials and training resources available skip right over how to write testable code because, being samples, they tend to not involve the typical layers you find in real code where you have services layers, data layers, etc. Inevitably, when you go to test your code that does have these dependencies, the tests are very slow, difficult to write, and often break as the underlying dependencies return results other than expected.
Background
Code that is well written is separated into layers, with each layer responsible for a different slice of the application. Actual layers vary based upon need and also upon developer habits, but a common scheme is:
- User Interface/Presentation Layer: This is your presentation logic and UI interaction code.
- Business Logic/Services Layer: This is your business logic. E.g.: code for a shopping cart. This shopping cart knows how to calculate the cart total, how to count items on the order, etc.
- Data Access Layer/Persistence Layer: This code knows how to connect to the database and return a shopping cart, or how to save a cart to the database.
- Database: This is where the cart's contents are saved.
Without Dependency Management
Without dependency management, when you write tests for the Presentation Layer, your code hooks into real services that hook into real data access code and then touch the real database. Really, when you are testing the "add to cart" feature or the "get cart item count", you want to test the code in isolation and be able to guarantee predictable results from your code. Without dependency management, your UI tests for "add to cart" are slow, and your dependencies return unpredictable results which can cause your test to fail.
The Solution is Dependency Injection
The solution to this problem is Dependency Injection. Dependency Injection or DI often seems confusing and complex to those who haven't done it, but in reality, it is a very, very simple concept and process with a few basic steps. What we want to do is centralize your dependencies, in this case, the use of the ShoppingCart
object, and then loosely couple your code so that when you run your app, it uses real services, and when you test it, you can use fake services that are fast and dependable. Note that there are several approaches you can take; to keep it simple, I am just demonstrating constructor injection.
Step 1 - Identify Your Dependencies
Dependencies are when your code is touching other layers. E.g., when your Presentation Layer touches the Services Layer. Your presentation code depends on the services layer, but we want to test the presentation code in isolation.
public class ShoppingCartController : Controller
{
public ActionResult GetCart()
{
ShoppingCartService shoppingCartService = new ShoppingCartService();
ShoppingCart cart = shoppingCartService.GetContents();
return View("Cart", cart);
}
public ActionResult AddItemToCart(int itemId, int quantity)
{
ShoppingCartService shoppingCartService = new ShoppingCartService();
ShoppingCart cart = shoppingCartService.AddItemToCart(itemId, quantity);
return View("Cart", cart);
}
Step 2 - Centralize Your Dependencies
While there are several ways this can be done, in this example, I am going to create a member variable of type ShoppingCartService
and then assign it to an instance that I will create in the constructor. In each place where I use ShoppingCartService
, I will then re-use this instance rather than creating a new instance.
public class ShoppingCartController : Controller
{
private ShoppingCartService _shoppingCartService;
public ShoppingCartController()
{
_shoppingCartService = new ShoppingCartService();
}
public ActionResult GetCart()
{
ShoppingCart cart = _shoppingCartService.GetContents();
return View("Cart", cart);
}
public ActionResult AddItemToCart(int itemId, int quantity)
{
ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
return View("Cart", cart);
}
}
Step 3 - Loose Coupling
Program against an interface rather than against concrete objects. If you write your code against IShoppingCartService
as an interface rather than against the concrete ShoppingCartService
, when you go to test, you can swap in a fake shopping cart service that implements IShoppingCartService
. In the image below, note that the only change is that the member variable is now of type IShoppingCartService
instead of just ShoppingCartService
.
public interface IShoppingCartService
{
ShoppingCart GetContents();
ShoppingCart AddItemToCart(int itemId, int quantity);
}
public class ShoppingCartService : IShoppingCartService
{
public ShoppingCart GetContents()
{
throw new NotImplementedException("Get cart from Persistence Layer");
}
public ShoppingCart AddItemToCart(int itemId, int quantity)
{
throw new NotImplementedException("Add Item to cart then return updated cart");
}
}
public class ShoppingCart
{
public List<product> Items { get; set; }
}
public class Product
{
public int ItemId { get; set; }
public string ItemName { get; set; }
}
public class ShoppingCartController : Controller
{
private IShoppingCartService _shoppingCartService;
public ShoppingCartController()
{
_shoppingCartService = new ShoppingCartService();
}
public ActionResult GetCart()
{
ShoppingCart cart = _shoppingCartService.GetContents();
return View("Cart", cart);
}
public ActionResult AddItemToCart(int itemId, int quantity)
{
ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity);
return View("Cart", cart);
}
}
Step 4 - Inject Dependencies
We now have all of our dependencies centralized in one place, and our code is now loosely coupled to those dependencies. As with before, there are several ways to handle the next step. Without having an IoC container such as NInject or StructureMap setup, the easiest way to do this is to just overload the constructor:
private IShoppingCartService _shoppingCartService;
public ShoppingCartController()
{
_shoppingCartService = new ShoppingCartService();
}
public ShoppingCartController(IShoppingCartService shoppingCartService)
{
_shoppingCartService = shoppingCartService;
}
Step 5 - Test with a Stub
An example of a possible test fixture for this is below. Note that I have created a fake (a.k.a. stub) of the ShoppingCartService
. This stub is passed into my Controller object, and the GetContents
method is implemented to return some fake data rather than calling code that actually goes to the database. As this is 100% code, it is much faster than querying a database, and I never have to worry about staging test data or cleaning up test data when I am finished testing. Note that because of Step 2 where we centralized our dependencies, I only have to inject it once. Because of Step 3, our dependency is loosely coupled, so I can pass in any object, real or fake, as long as it implements the IShoppingCartService
interface.
[TestClass]
public class ShoppingCartControllerTests
{
[TestMethod]
public void GetCartSmokeTest()
{
ShoppingCartController controller =
new ShoppingCartController(new ShoppingCartServiceStub());
ActionResult result = controller.GetCart();
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
}
public class ShoppingCartServiceStub : IShoppingCartService
{
public ShoppingCart GetContents()
{
return new ShoppingCart
{
Items = new List<product> {
new Product {ItemId = 1, ItemName = "Widget"}
}
};
}
public ShoppingCart AddItemToCart(int itemId, int quantity)
{
throw new NotImplementedException();
}
}
Where do I Go From Here?
- Use a IoC/DI container: The most common and popular IoC containers for .NET are StructureMap and Ninject. In real world code, you are going to have a lot of dependencies, your dependencies will have dependencies, etc. You will quickly find it becomes unmanageable. The answer is to use a DI/IoC framework to manage them.
- Use an Isolation Framework: Creating stubs and mocks can get to be a lot of work, and using a mocking framework can save you a lot of time and code. The most common for .NET are Rhino Mocks and Moq.