This short series of posts represents a snapshot of my
thinking at the end of 2011, in terms of how I would ideally produce web
applications using the .NET platform. In this, part 2, I’m going to cover my current testing strategy and preferences.
For this post, the plan is:
1. General
Testing
2. Endpoint
Testing, using the Endpoint Testing Pattern
3. Database
Testing with RavenDb
I won’t be talking about higher-level tests or BDD that
drive the TDD. The only variable there is the frameworks, which are generally
interchangeable.
General Testing
My unit tests need to be fast, and they need to be readable
– no interacting with databases then. See the last section for that.
To make unit tests readable I treat them as high-importance
code. This doesn’t mean I take any longer, it means I have three simple rules
that I nearly always use:
1. Keep
them short (3 lines is optimal, 7 means I don’t get to eat for the day)
2. Hide
assertions inside extension methods that read well (just like the GOOS boys)
3. Wrap
setup code in methods too
[Test]
public void Get_GivenIdForExistingBook_ShouldReturnViewModel_WithBooksDetails()
{
var book = GetRandomBookSimulatedToExist();
var viewModel = endpoint.Get(new UpdateBookLinkModel {Id = book.Id});
viewModel.ShouldHaveDetailsFor(book);
}
Above is an example of what I consider to be an ideal test.
Context Specifications
With the above preferences applied, most of the time there
are just 3 - 5 line test methods that follow like a given, when then. Sometimes,
though, it really makes sense to use a context specification.
[SetUp]
public void WhenRequestingABookReview_AndTheSystemContainsBooks_WithDifferentGenres()
{
var retriever = MockRepository.GenerateMock<IBookRetriever>();
var endpoint = new ViewEndpoint(retriever);
book = BookTestingHelper.GetBook(rating: 4);
retriever.Stub(r => r.GetById(book.Id)).Return(book);
SetupBookForSameGenreIncludingReviewBook();
retriever.Stub(r => r.GetReviewedBooks(book.Genre.Id)).Return(allBooksForSameGenre);
viewModel = endpoint.Get(new ViewBookLinkModel { Id = book.Id });
}
[Test]
public void ViewModelShouldHaveBooksTitle()
{
Assert.AreEqual(book.Title, viewModel.Title);
}
[Test]
public void ViewModelShouldHaveBooksGenreName()
{
Assert.AreEqual(book.Genre.Name, viewModel.GenreName);
}
[Test]
public void ViewModelShouldHaveBooksRating()
{
Assert.AreEqual(book.Rating, viewModel.Rating);
}
If you look at the
full
source code for this class you’ll see there are another 6 tests below it. I
think in this case it was the most readable way to test.
When Do They Run?
Always - these are the general tests that form part of the
main suite for giving immediate feedback about any regressions.
Endpoint Testing
In
part
1 my diagram mentioned “Endpoint-specific models”. This includes link
(usually called request), view, and input models. By having them specific to
each endpoint, the models are tested as part of the endpoint tests and not
alone. I refer to this mentallly as the endpoint testing pattern, but it is not an official
term.
For example:
[Test]
public void Get_GivenIdForExistingBook_ShouldReturnViewModel_WithBooksDetails()
{
var book = GetRandomBookSimulatedToExist();
var viewModel = endpoint.Get(new UpdateBookLinkModel {Id = book.Id});
viewModel.ShouldHaveDetailsFor(book);
}
This test covers the endpoint and the models. There would be
no additional tests for the models – they only exist to serve one action method
on this endpoint so don’t need it.
Update Scenario
[Test]
public void Post_GivenUpdateModel_ShouldCreateDtoAndPassToBookUpdater()
{
var model = new UpdateBookInputModel
{
Genre = "genres/9",
BookStatus = BookStatus.Reviewed,
Title = "Updated title",
Id = "books/444",
Rating = 3,
Description_BigText = "Updated description",
};
endpoint.Post(model);
updater.ShouldHaveBeenCalledWithDtoMatching(model);
}
For these scenarios, a simple assertion is made that a
service was called. Anything more involved than that is probably logic that
doesn’t live there in an ideal world.
When Do They Run?
These will be ultra-fast tests that are part of the main
suite of tests. Every time there is a change in code, these get run to check
for regressions.
Database Testing With RavenDb
I’ve specifically name-dropped RavenDb because
this is my utopia. In this world, we eat, sleep and breathe RavenDb for data
access because it makes life a lot easier. You get all the benefits of
NHibernate’s session, transactions & POCOs, without any of the pain of the
SQL mapping or performance disasters.
Another benefit of RavenDb is the testing support. In fact,
this is fantastic. A mini Ayende sits their inside your pc spinning up an
instance of his document database in your computer’s memory. As far as
I’m aware, this works exactly the same as a normal instance of the database.
None of those SQL Lite – SQL Server mismatches.
Here’s a data
access test:
[Test]
public void GetWishlistBooks_ShouldOnlyReturnBooks_OnTheWishlist()
{
PopulateSessionWithBooksOfDifferentStatus();
Session.SaveChanges();
var fromSession = Session.Query<Book>().Where(b => b.Status == BookStatus.Wishlist);
var fromRetriever = retriever.GetWishlistBooks();
Assert.AreEqual(fromSession, fromRetriever);
}
Here is how the session is setup for testing….
public abstract class RavenTestsBase
{
protected IDocumentSession Session;
private EmbeddableDocumentStore store;
[SetUp]
public void SetUp()
{
store = new EmbeddableDocumentStore
{
Configuration = {RunInMemory = true,}
};
store.Initialize();
Session = store.OpenSession();
}
[TearDown]
public void TearDown()
{
Session.Dispose();
}
}
…..I agree - someone is taking the piss. This is so simple
I’m worried the cleaner will take my job.
So You Run This With Your Main Test Suite?
They’re fast, but they’re no Ussain Bolt on happy pills….. I
run these when any data access code changes and before important commits.
Conclusion
I hope you enjoyed reading about my testing preferences and
conventions. Whether you agree with them or not, it’s important to tell
yourself how you think the best way to do something is. This gives you a point
of reference to take on and incorporate new information – it helps you to
improve… and socialise with other developers in the pub.
The most important part is having conventions about how you
test. This lets you create consistent tests, that require less cognitive
resource to create, maintain and read. You can then put more focus on
delivering software and understanding the problem domain.
In part 3 I’ll show you some of my other preferences for web
frameworks. Most of them have been learned by playing with FubuMVC.