Making my Repository Factory Test-Friendly

Intro

In this blog post, only last week, I worked through the steps I took to implement Code First in my website using BDD (Behaviour Driven Development) and my existing class hierarchy based on the Repository Pattern. Importantly, remembering that an instance of my interface IDataContext was passed to the repository factory and this encapsulated interaction with the underlying data store.

Well, last week, I implemented an IDataContext that spoke to my database — and I confirmed this by using SpecFlow, the BDD tool, to write some specs and then run tests to confirm those specs. But I cannot really use this setup for testing all of my application, because I don’t want to interact with my database.

With a week passing by, and now being the weekend again (at the moment I am reading Pro ASP.NET MVC 2 on weeknights, plus gym and social stuff) I have the chance to continue work on the re-design of my website NTCoding.com. So…..what challenge awaits me this week?

This week I am going to create another IDataContext implementer, but this time it is for testing purposes, and as alluded to above, I don’t want to be talking to any databases (although I may eventually look into some “light databases”). Some collections in memory should be fine for my specs (according to YAGNI).

Knowing that all I need to do is adhere to the requirements of the IDataContext interface, I can be sure that as long as my new implementer of IDataContext meets this interface’s requirements then it will work happily with my repositories and I can start including those repositories in my higher-level tests.Writing the Specs

As per my last blog post, I’ll be writing specs, using SpecFlow for all my tests — even unit tests such as this. And again, this week should be quite easy in terms of writing specs because I can base them on the requirements of the interface.

Just to recap, here is the IDataContext interface, remembering that an instance of this is passed into the repository factory to provide access to all the domain entities previously added. You may also note that there is now a CancelChanges() method — a requirement driven by the new behaviours of the system.

public interface IDataContext

{

void Add(T newItem) where T : class;

IEnumerable Where(Expression<Func<T, bool>> expression) where T : class;

IEnumerable FindAll() where T : class;

void Delete(T itemToDelete) where T : class;

void SaveChanges();

void CancelChanges();

}

Below are the specs I wrote that will need to pass in order to confirm the new data context works as intended. As I mentioned above, it is quite easy when there is an interface your behaviour is based on.

Feature: CRUD Operations

In order to test parts of my application that interact with a database

As a Developer

I need a testing data context that doesn’t interact with a database

Scenario: Addition With Save Changes

Given I have added an item

And I save changes

When I try to retrieve the item

Then I should get the item back

Scenario: Addition Without Save Changes

Given I have added an item

And I don’t save changes

When I try to retrieve the item

Then I should not get the item back

Scenario: Deletion With Save Changes

Given I have added an item

And I save changes

And I delete the item

And I save changes

When I try to retrieve the item

Then I should not get the item back

Scenario: Deletion Without Save Changes

Given I have added an item

And I save changes

And I delete the item

And I don’t save changes

When I try to retrieve the item

Then I should get the item back

Scenario: Querying

Given I have added a book with the title Nick’s Book

And I save changes

When I query for books with the title Nick’s Book

Then I should retrieve a matching item

Scenario: FindAll

Given I have supplied ten books

And I save changes

When I request all books

Then I should receive ten booksImplementing the Specs

Working through the specs sequentially, and logically you could argue, my first task was to be able to add items to the context and to make sure they were being stored.

Because implementing each step is pretty obvious, I’ll just run through the first so you can understand my coding process (and hopefully comment on how you think I can improve or how you do things differently). After that I’ll discuss purely the new implementer of IDataContext

I was then kindly told by SpecFlow, to implement these steps:

[Given(@”I have added an item”)]

public void GivenIHaveAddedAnItem()

{

}

[Given(@”I save changes”)]

public void GivenISaveChanges()

{

}

[When(@”I try to retrieve the item”)]

public void WhenITryToRetrieveTheItem()

{

}

[Then(@”I should get the item back”)]

public void ThenIShouldGetTheItemBack()

{

}

Immediately with the first step, the implication is to interact with the data context. So in a BDD way, I specified how I wanted the code to work before implementing it (calling methods on objects that don’t exist).

[Given(@”I have added an item”)]

public void GivenIHaveAddedAnItem()

{

DataContext.Add(_book);

}

Implementing the subsequent three steps in the same way, I crafted:

[Given(@”I save changes”)]

public void GivenISaveChanges()

{

DataContext.SaveChanges();

}

[When(@”I try to retrieve the item”)]

public void WhenITryToRetrieveTheItem()

{

if(DataContext.FindAll().Count() > 0)

{

_returnedBook = DataContext.FindAll().First();

}

}

[Then(@”I should get the item back”)]

public void ThenIShouldGetTheItemBack()

{

Assert.IsNotNull(_returnedBook);

Assert.IsTrue(_returnedBook.Title == _book.Title);

}

Please note: book is an entity that already exists in this application

At the moment, I have no real entities, just calls to objects that don’t exist — based on the required behaviour (BDD).

So, I then used my buddy ReSharper to generate the DataContext class for me. After then telling the class to implement IDataContext, naming it TestDataContext, and implementing the Add feature, it looked like so:

public class TestDataContext : IDataContext

{

private List DataStore;

private List _unconfirmedAdditions;

public TestDataContext()

{

DataStore = new List();

_unconfirmedAdditions = new List();

}

public void Add(T newItem) where T : class

{

_unconfirmedAdditions.Add(newItem);

}

public IEnumerable Where(Expression<Func<T, bool>> expression) where T : class

{

}

public IEnumerable FindAll() where T : class

{

}

public void Delete(T itemToDelete) where T : class

{

}

public void SaveChanges()

{

AcceptUnconfirmed();

ClearUnconfirmed();

}

private void AcceptUnconfirmed()

{

_unconfirmedAdditions.ForEach(DataStore.Add);

}

public void CancelChanges()

{

ClearUnconfirmed();

}

private void ClearUnconfirmed()

{

_unconfirmedAdditions.Clear();

}

}

When adding the item, it should not be saved until the changes are confirmed, via SaveChanges(), therefore you can observer that I am initially saving them in a separate collection that only marks them for additionTestDataContext

Okay, after doing the rest of my specs and them implementing them (“like a good developer”), I had created the following masterpiece of an IDataContext implementer:

public class TestDataContext : IDataContext

{

private List DataStore;

private List _unconfirmedAdditions;

private List _unconfirmedDeletions;

public TestDataContext()

{

DataStore = new List();

_unconfirmedAdditions = new List();

_unconfirmedDeletions = new List();

}

public void Add(T newItem) where T : class

{

_unconfirmedAdditions.Add(newItem);

}

public IEnumerable Where(Expression<Func<T, bool>> expression) where T : class

{

var strongList = new List();

DataStore.ForEach(i =>

{

if(i is T)

{

strongList.Add(i as T);

}

});

return strongList.AsQueryable().Where(expression);

}

public IEnumerable FindAll() where T : class

{

foreach (var item in DataStore)

{

if(item is T)

{

yield return item as T;

}

}

}

public void Delete(T itemToDelete) where T : class

{

_unconfirmedDeletions.Add(itemToDelete);

}

public void SaveChanges()

{

AcceptUnconfirmed();

ClearUnconfirmed();

}

private void AcceptUnconfirmed()

{

_unconfirmedAdditions.ForEach(DataStore.Add);

_unconfirmedDeletions.ForEach(d => DataStore.Remove(d));

}

public void CancelChanges()

{

ClearUnconfirmed();

}

private void ClearUnconfirmed()

{

_unconfirmedAdditions.Clear();

_unconfirmedDeletions.Clear();

}

}

The most important point to note is that instead of connecting to a database, or some ORM tool (Code First), I just have a plain List, named _collection. Obviously this is going to mean a lot of casting in scenarios where I return strongly-typed collections. But in the circumstance I feel that is fine and the risk is minimal — especially when I have specs that confirm the behaviour and they continue to pass.

With Add() and Delete() I am just marking the objects for addition or deletion. This enables me to CancelChanges(), and lose any unsaved operations performed on the data context. When changes are committed, via SaveChanges() the objects to be added are merged into the main collection, whilst the objects marked for deletion are removed from the main collection — and both these transient collections are reset.

If you look in FindAll() or Where(), there is a little bit of groovy stuff going on, but with the data container being just a List, type-checking and casting is essential. With the use of LINQ, scenarios such as these can be quite concise anyway.Conclusion

By working to the principles of good OO programming, I have seen continual improvements to the quality of my code. In this case, programming to interfaces — allowing me to switch out one implementer of IDataContext for another, facilitating the ability test parts of my application that would normally make round-trips to the database (nearly all of it).

In this example, I have also further endorsed my feelings that BDD, using SpecFlow, engenders a more organised work flow and a more maintainable code base — an improvement on TDD — a drastic improvement on what I had been used to with constraints placed by Web Forms.

And finally, it feels really nice to have established this BDD-style workflow; it is an organised process that provides the safety of knowing important parts of your code-base are working as intended, whilst you get to write your specs in English and not contrive unit test method names.

Times are good.

See you soon