Unit Testing with a Data Context
Recently I surveyed how people solve the problem of unit testing with complex objects such as the data context from LinqToSql. What I found is that it can be solved in several different ways. However, it seemed no one source identified more than a single way to do it. In my review each approach had merits as well as considerations to make. Useful to me, and hopefully to you, is to have something (this blog) which shows three different approaches to tackling the "how to test linqtosql" problem, and take the opportunity to assess each option. Also I'll provide source code for the entire thing so regardless of which approach is best for you, you can get up and running quickly.
Why do we want to test LinqToSql? The short answer is we don't. That is, I don't want to test that my database connection is working, and I don't want to test my ability to insert, retrieve, and delete records. But what I do want to test is the conditional logic which goes into the queries. "How do I know my query is selecting the correct record?" By having unit tests constructed around this logic it helps me in two ways:
- I have some certainty that the logic I wrote is doing what I intended it to do. And I have the opportunity to setup as many test cases as I need to handle all the fringe scenarios which can happen.
- My code is now re-factor proof. The next developer who comes along cannot change the outcome of my methods without breaking the tests. This applies to my LinqToSql methods. If I write a method which says, "Get me all the paragraphs in a chapter which contain a specific phrase", now I have some guarantee that however the method is implemented, it will continue to do just that.
We’ll take the TPIR approach (The Price Is Right) and offer 3 doors from which you may choose. However, unlike TPIR, I'm going to tell you what is behind each one before we get into the details. Spoiler alert.
The Scenario
For today, let's say you have an existing project. There is already a lot of LinqToSql code, and you wish to increase the unit test coverage. We'll use NUnit as our basic testing framework. We also have a database schema, we'll use something straightforward which looks a lot like this:
Then we'll have some methods we want to test which retrieve paragraphs from a book in a couple different ways. Now I’m not proposing that the following code is good design, but let’s say it’s what you have. Which is, a partial class of the BooksDataContext which adds additional methods to the data context object.
public partial class BooksDataContext
{
///
/// Gets the paragraphs which contain the phrase for a given book and chapter
///
/// The book id.
/// The chapter id.
/// The phrase.
///
public IList GetParagraphs(int bookId, int chapterId, string phrase)
{
IQueryable paragraphs = Paragraphs.Where(x =>
x.Chapter.Id == chapterId
&& x.Chapter.Book.Id == bookId
&& x.Text.Contains(phrase));
return paragraphs.ToList();
}
///
/// Gets the paragraph by index for a given book and chapter
///
/// The book id.
/// The chapter id.
/// Index of the paragraph.
///
public Paragraph GetParagraph(int bookId, int chapterId, int paragraphIndex)
{
Paragraph paragraph = Paragraphs.Where(x =>
x.ChapterId == chapterId
&& x.Chapter.Book.Id == bookId)
.Skip(paragraphIndex)
.FirstOrDefault();
return paragraph;
}
}
The thing we want to test is all this logic rolled up into the Linq statement. This is the business or application logic which we want to make sure does what it claims, and we want to protect it from unintentional modifications. That is, GetParagraphs should always return the paragraphs which contain my phrase. And GetParagraph should always return the paragraph at the given index.
Here is a made-up example of of how we might use our GetParagraph() method in our application:
public string GetFirstParagraph()
{
string firstParagraph = string.Empty;
using (BooksDataContext booksDataContext = new BooksDataContext())
{
Paragraph paragraph = booksDataContext.GetParagraph(1, 1, 0);
if (paragraph != null)
{
firstParagraph = paragraph.Text;
}
}
return firstParagraph;
}
Door Number 1: Isolation Frameworks
The hard thing to test about this is the data context itself. I don’t really want to build and tear down real database tables. Enter into the picture, JustMock. JustMock is from Telerik and is an isolation framework. If you are familiar with mocking frameworks, then this is a mocking framework on steroids. It can "Test the Untestable". In our case, it can run tests directly on the BooksDataContext object which a traditional mocking framework cannot do. There are other isolation frameworks such as TypeMock, and Microsoft has one named Fakes for VS2012 and one named Moles for VS2008/2010. To use JustMock, the beauty is that we can use our existing code as is. The following are examples of unit tests using our BooksDataContext directly on our LinqToSql methods.
[TestClass]
public class ParagraphRepositoryTestsJustMock
{
private BooksDataContext _booksDataContext;
private string _text0;
private string _text1;
private string _text2;
[TestFixtureSetUp]
public void SetUp()
{
_booksDataContext = Mock.Create();
Mock.Arrange(() => _booksDataContext.Paragraphs).ReturnsCollection(GetParagraphs());
}
///
/// Build the test content
///
///
private IEnumerable GetParagraphs()
{
_text0 = "His name was Gaal Dornick and he was just a country boy who had never seen Trantor before. That is, not in real life. " +
"He had seen it many times on the hyper-video, and occasionally in tremendous three-dimensional newscasts covering an Imperial Coronation of the opening of a Galactic Council. " +
"Even though he had lived all his life on the world of Synnax, which circled a star at the edges of the Blue Drift, he was not cut off from civilization, you see. " +
"At that time, no place in the Galaxy was.";
_text1 = "There were nearly twenty-five million inhabited planets in the Galaxy then, and not one but owed allegiance to the Empire whose seat was on Trantor. " +
"It was the last half-century in which that could be said.";
_text2 = "To Gaal, this trip was the undoubted climax of his young, scholarly life. He had been in space before so that the trip, as a voyage and nothing more, meant little to him. " +
"To be sure, he had traveled previously only as far as Synnax's only satellite in order to get the data on the mechanics of meteor driftage which he needed for his dissertation, " +
"but space-travel was all one whether one travelled half a million miles, or as many light years.";
Book book = new Book() { Id = 1, Title = "Foundation", Author = "Isaac Asimov" };
Chapter chapter = new Chapter() { Id = 1, Title = "The Psychohistorians", Book = book };
Paragraph paragraph0 = new Paragraph() { Id = 1, Text = _text0, Chapter = chapter };
Paragraph paragraph1 = new Paragraph() { Id = 2, Text = _text1, Chapter = chapter };
Paragraph paragraph2 = new Paragraph() { Id = 3, Text = _text2, Chapter = chapter };
List paragraphs = new List();
paragraphs.Add(paragraph0);
paragraphs.Add(paragraph1);
paragraphs.Add(paragraph2);
return paragraphs;
}
[Test]
public void GetParagraphsTest()
{
// act
IList paragraphsGalaxy = _booksDataContext.GetParagraphs(1, 1, "Galaxy");
IList paragraphsEmpire = _booksDataContext.GetParagraphs(1, 1, "Empire");
// assert
Assert.AreEqual(2, paragraphsGalaxy.Count);
Assert.AreEqual(1, paragraphsEmpire.Count);
}
[Test]
public void GetParagraphTest()
{
// act
Paragraph paragraph = _booksDataContext.GetParagraph(1, 1, 1); // get the second paragraph
// assert
Assert.AreEqual(_text1, paragraph.Text);
}
}
The benefit of using JustMock is that aside from our unit test we didn't need to write any additional code. Depending on your situation this could be great, and it could be your only option. The drawback, is it allows us to write testable code without necessarily writing well architected code. The later options we will see encourage us to write better architected, more testable code.
The other consideration for JustMock and TypeMock is that these are not free products. And the Microsoft frameworks require additional consideration because they are specific to what version of Visual Studio you are on. Finally, for Moles, I found that support for using testing frameworks other than MTest required additional setup.
Note 1: I used the trial version of JustMock. So although the source code I used is supplied here, you will need to acquire your own version of JustMock to execute this implementation.
Note 2: JustMock offers a free edition. Although we use RhinoMock in Scenario 3, the free version of JustMock offers this functionality as well.
Door Number 2: In Memory Testing
The next approach we’ll look at may require some re-factoring of your code to make it more testable. However it will allow unit testing without any additional frameworks which makes things a bit simpler and may be ideal for your situation.
In this approach, which we’ll also carry into the third implementation, we’ll start by making ourselves an IDataContext class to abstract out the LinqToSql data context.
public interface IDataContext
{
IQueryable Repository() where T : class;
void Insert(T item) where T : class;
void Delete(T item) where T : class;
void SubmitChanges();
}
I’ve only called for some basic operations in this interface. This may be good for most things you’ll need, but you can add more if needed. The Repository method is our accessor to the data storage medium. Insert/Delete/SubmitChanges are self-explanatory.
Next we’ll make an implementation of this interface for LinqToSql:
///
/// LinqToSql specific data context
///
public class LinqToSqlDataContext : IDataContext
{
private readonly BooksDataContext _context;
public LinqToSqlDataContext(BooksDataContext context)
{
_context = context;
}
///
/// Gets the repository for the given type of entities
///
/// The type of the entity
/// The repository of the given type
public IQueryable Repository() where T : class
{
Table table = _context.GetTable();
return table;
}
///
/// Deletes the specified entity from the repository
///
/// The type of the entity
/// The entity to delete
public void Delete(T item) where T : class
{
ITable table = _context.GetTable();
table.DeleteOnSubmit(item);
}
///
/// Adds a new entity to the repository
///
/// The type of the entity
/// The entity to add
public void Insert(T item) where T : class
{
ITable table = _context.GetTable();
table.InsertOnSubmit(item);
}
///
/// Submits the changes.
///
public void SubmitChanges()
{
_context.SubmitChanges();
}
}
In this implementation, we are passing in our BooksDataContext to the constructor, then we use some standard code to implement each of the IDataContext operations.
Next we need to move our queries out of our data context’s partial class and into a business class. Something like this:
///
/// Calls to the Paragraph Repository
///
public class ParagraphRepository
{
private readonly IDataContext _context;
public ParagraphRepository(IDataContext context)
{
_context = context;
}
///
/// Gets the paragraphs which contain the phrase for a given book and chapter
///
/// The book id.
/// The chapter id.
/// The phrase.
///
public IList GetParagraphs(int bookId, int chapterId, string phrase)
{
IQueryable paragraphs = _context.Repository().Where(x =>
x.Chapter.Id == chapterId
&& x.Chapter.Book.Id == bookId
&& x.Text.Contains(phrase));
return paragraphs.ToList();
}
///
/// Gets the paragraph by index for a given book and chapter
///
/// The book id.
/// The chapter id.
/// Index of the paragraph.
///
public Paragraph GetParagraph(int bookId, int chapterId, int paragraphIndex)
{
Paragraph paragraph = _context.Repository().Where(x =>
x.ChapterId == chapterId
&& x.Chapter.Book.Id == bookId)
.Skip(paragraphIndex)
.FirstOrDefault();
return paragraph;
}
}
Here’s the significant part about this class: we’re passing in our IDataContext. This could be our LinqToSql implementation, but it could also be any implementation we want. We could make an Entity data source, a Text File data source, or even Access (eeeew!). We have a lot more flexibility now. As long as it implements our interface, the data source doesn't matter. Now the code we want to test has separation from the data context, this is key.
Good so far. We still want to be able to test the Linq code in our ParagraphRepository class. Our LinqToSqlDataContext isn't quite what we want because it wants a legit LinqToSql data context. Enter, the InMemoryDataContext class.
Our InMemoryDataContext implementation uses a List
You’ll see this is very straightforward. The Repository method returns the objects of the type we are looking for. The Insert/Delete methods simply add and remove items from our List. Since a list doesn’t require a Submit step like LinqToSql does, we have this one calling an event which can be useful for testing to make sure it was called.
Now we have everything we need to unit test our code. The following unit tests run against the exact same Linq as our application, but we are using our InMemoryDataSource implementation of IDataSource as the repository.
[TestFixture]
public class ParagraphRepositoryTests
{
private IDataContext _iDataContext;
private string _text0;
private string _text1;
private string _text2;
[TestFixtureSetUp]
public void SetUp()
{
_iDataContext = new InMemoryDataContext(GetParagraphs());
}
///
Door Number 3: Mocking Frameworks
The last model was good, it promotes reasonable design and our code is testable. But what about that InMemoryDataContext implementation? It was cool, but we really only made this implementation to support our unit tests. If we could drop this extra code, I’d like that a lot. Enter RhinoMock. Any standard mocking framework will do, MOQ is another good one.
True, we are adding YAF (yet another framework) to our project. But this one bothers me not, mocking frameworks are fairly standard. What we’re going to do is keep IDataContext, keep our LinqToSqlDataContext implementation, and keep our ParagraphRepository as is. From here, with the addition of the mocking framework we can go straight to Unit Testing!
[TestFixture]
public class ParagraphRepositoryTestsRhinoMocks
{
private IDataContext _iDataContext;
private string _text0;
private string _text1;
private string _text2;
[TestFixtureSetUp]
public void SetUp()
{
// mock the IDataContext
_iDataContext = MockRepository.GenerateStub
Summary
Whether these options made you laugh (or cry), or if they gave you an Ah Ha moment, or perhaps even a lightbulb, I thank you for reading. These options are all found individually and many times over out in the misty BG (blogiverse-googlesphere). My goal is to bring them together in one place so these unit tests can meet and have a little party. I'm sure there is a fourth door, maybe even a fifth! Thanks Bob, I'll take door number 3 for now.
Resources
Visual Studio Solution
Unit Testing Frameworks:
Isolation Frameworks:
Mocking Frameworks