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 books
Implementing 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<Book>().Count() > 0)
{
_returnedBook =
DataContext.FindAll<Book>().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<object>
DataStore;
private
List<object>
_unconfirmedAdditions;
public
TestDataContext()
{
DataStore = new List<object>();
_unconfirmedAdditions = new List<object>();
}
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 addition
TestDataContext
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<object>
DataStore;
private
List<object>
_unconfirmedAdditions;
private
List<object>
_unconfirmedDeletions;
public
TestDataContext()
{
DataStore = new List<object>();
_unconfirmedAdditions = new List<object>();
_unconfirmedDeletions = new List<object>();
}
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