Introduction to Behavior Driven Development and Mocking in .Net
Warning
This is a long and dense post that will take a lot of concentration to get through. Also, this is entirely my opinion on what I consider to be "effective" unit testing, so take the time to digest what is written and make your own evaluations (think critically).
Setting the Stage
I don't want to waste your time, because I know it's valuable. Also, if you have any questions or need any clarifications, please don't hesitate to email me (ar@amirrajan.net). So before you start reading, make sure:
- You really want to learn how to effectively unit test.
- You understand dependency injection.
- You know how to get around with MSTest or NUnit.
I really don't like trivial examples. So again, this is pretty dense code that is actually being used in one of my web sites (borrowedgames.com). So take the time to really take it in. Let's start drinking from the fire hose shall we?
IMessageManager
In my web site, I have added messaging capabilities (you can send and receive internal messages from a user...very similar to what you would see in Facebook or LinkedIn). Here is the interface specification for my "MessageManager".
public interface IMessageManager
{
void SendMessage(Guid senderUserId,
Guid recipientUserId,
string subject,
string body,
DateTime sendDate);
void MarkMessageAsRead(Guid ownerUserId, Guid messageId);
void DeleteInboxMessage(Guid ownerUserId, Guid messageId);
void DeleteOutboxMessage(Guid ownerUserId, Guid messageId);
}
MessageManager : IMessageManager
And this is the concrete implementation of the IMessageManager. We'll go through this code line by line so we understand what we will be testing.
public class MessageManager : IMessageManager
{
private IUserInboxSaver _userInboxSaver;
private IUserOutboxSaver _userOutboxSaver;
private IUserInboxRetriever _userInboxRetriever;
private IUserOutboxRetriever _userOutboxRetriever;
public MessageManager(IUserInboxSaver userInboxSaver,
IUserOutboxSaver userOutboxSaver,
IUserInboxRetriever userInboxRetriever,
IUserOutboxRetriever userOutboxRetriever)
{
_userInboxSaver = userInboxSaver;
_userOutboxSaver = userOutboxSaver;
_userInboxRetriever = userInboxRetriever;
_userOutboxRetriever = userOutboxRetriever;
}
void IMessageManager.SendMessage(Guid senderUserId,
Guid recipientUserId,
string subject,
string body,
DateTime sendDate)
{
_userInboxSaver.SaveInboxMessage(Guid.NewGuid(),
senderUserId,
recipientUserId,
subject,
body,
true,
sendDate);
_userOutboxSaver.SaveOutboxMessage(Guid.NewGuid(),
senderUserId,
recipientUserId,
subject,
body,
sendDate);
}
void IMessageManager.MarkMessageAsRead(Guid ownerUserId, Guid messageId)
{
if(_userInboxRetriever.InboxMessageExists(ownerUserId, messageId))
{
_userInboxSaver.SetIsNew(messageId, false);
}
}
void IMessageManager.DeleteInboxMessage(Guid ownerUserId, Guid messageId)
{
if(_userInboxRetriever.InboxMessageExists(ownerUserId, messageId))
{
_userInboxSaver.DeleteInboxMessage(messageId);
}
}
void IMessageManager.DeleteOutboxMessage(Guid ownerUserId, Guid messageId)
{
if (_userOutboxRetriever.OutboxMessageExists(ownerUserId, messageId))
{
_userOutboxSaver.DeleteOutboxMessage(messageId);
}
}
}
MessageManager Dependencies
Notice that the concrete implementation of MessageManager depends on the following repository/database interfaces: IUserInboxSaver, IUserOutboxSaver, IUserInboxRetriever, IUserOutboxRetriever. Here are the interface definitions.
public interface IUserInboxSaver
{
void SaveInboxMessage(Guid messageId,
Guid senderUserId,
Guid recipientUserId,
string subject,
string body,
bool isNew,
DateTime sendDate);
void SetIsNew(Guid messageId, bool isNew);
void DeleteInboxMessage(Guid messageId);
}
public interface IUserOutboxSaver
{
void SaveOutboxMessage(Guid messageId,
Guid senderUserId,
Guid recipientUserId,
string subject,
string body,
DateTime sendDate);
void DeleteOutboxMessage(Guid messageId);
}
public interface IUserInboxRetriever
{
bool InboxMessageExists(Guid ownerUserId, Guid messageId);
List<IMessage> GetNewInboxMessages(Guid ownerUserId);
List<IMessage> GetReadInboxMessages(Guid ownerUserId);
IMessageWithBody GetInboxMessage(Guid ownerUserId, Guid messageId);
}
public interface IUserOutboxRetriever
{
bool OutboxMessageExists(Guid ownerUserId, Guid messageId);
List<IMessage> GetOutboxMessages(Guid ownerUserId);
IMessageWithBody GetOutboxMessage(Guid ownerUserId, Guid messageId);
}
Line by Line
So far so good? Alright, time for the line by line. Let's start with the constructor.
private IUserInboxSaver _userInboxSaver;
private IUserOutboxSaver _userOutboxSaver;
private IUserInboxRetriever _userInboxRetriever;
private IUserOutboxRetriever _userOutboxRetriever;
public MessageManager(IUserInboxSaver userInboxSaver,
IUserOutboxSaver userOutboxSaver,
IUserInboxRetriever userInboxRetriever,
IUserOutboxRetriever userOutboxRetriever)
{
_userInboxSaver = userInboxSaver;
_userOutboxSaver = userOutboxSaver;
_userInboxRetriever = userInboxRetriever;
_userOutboxRetriever = userOutboxRetriever;
}
Pretty straight forward constructor. MessageManager depends on a few repository interfaces to store messages in the database. Each user has an Inbox and an Outbox....and the retrievers and savers facilitate the persistence and retrieval of the messages. Now for the SendMessage() method.
void IMessageManager.SendMessage(Guid senderUserId,
Guid recipientUserId,
string subject,
string body,
DateTime sendDate)
{
_userInboxSaver.SaveInboxMessage(Guid.NewGuid(), //messageId
senderUserId, //senderId
recipientUserId, //recipientId
subject, //subject
body, //body
true, //isNew
sendDate); //datesent
_userOutboxSaver.SaveOutboxMessage(Guid.NewGuid(), //messageId
senderUserId, //senderId
recipientUserId, //recipientId
subject, //subject
body, //body
sendDate); //sendDate
}
The SendMessage() method, adds the message entry in the sender's outbox and inserts the same message in the recipients inbox. Also, the inbox message is marked as a new message. Since Guid.NewGuid() is passed in for the messageId, a message would get inserted in both the sender's outbox and the recipient's inbox (the messageId is a primary key in the database). Now for the MarkMessageAsRead() method.
void IMessageManager.MarkMessageAsRead(Guid ownerUserId, Guid messageId)
{
if(_userInboxRetriever.InboxMessageExists(ownerUserId, messageId))
{
_userInboxSaver.SetIsNew(messageId, false);
}
}
Given the owner and the message id, the repository is queried to see if the message exists. If it does then the message's IsNew property is set to false using _userInboxSaver.SetIsNew(). The method would go to the database, find the message and set the IsNew column to 0 for that particular message. Now for DeleteInboxMessage() and DeleteOutboxMessage().
void IMessageManager.DeleteInboxMessage(Guid ownerUserId, Guid messageId)
{
if(_userInboxRetriever.InboxMessageExists(ownerUserId, messageId))
{
_userInboxSaver.DeleteInboxMessage(messageId);
}
}
void IMessageManager.DeleteOutboxMessage(Guid ownerUserId, Guid messageId)
{
if (_userOutboxRetriever.OutboxMessageExists(ownerUserId, messageId))
{
_userOutboxSaver.DeleteOutboxMessage(messageId);
}
}
Each method first queries the repository to ensure that the message exists for the owner and the message id. If the combination exists then the respective delete method is called. A delete sql statement would get generated and executed inside of the DeleteInboxMessage() and DeleteOutboxMessage() methods.
The Unit Tests (BDD Style)
Whew...that was a lot of explanation (make sure you understand what MessageManager is doing before reading on)...now that the stage is set, let's look at how to test this class.
Naming
The key to naming your tests is to make them as human readable as possible. It is 100% okay to have class names that are really long (and method names that are even longer). You want these unit tests to document the behavior of the code so that any developer can jump in, look at the unit tests, and have a pretty good understanding of what you are trying to do.
The .cs Files and the Methods
You'll have many .cs files to fully test one class. In the case of MessageManager, I had to create four .cs files. Each .cs file has behavioral style methods....you'll probably have a .cs file for each method in a class (use your best judgement on this...start with one .cs file...the very moment you see your test initialization getting too complicated, start breaking it up). Here are the names of the .cs files and the method names:
-
when_sending_message.cs
- should_insert_message_into_senders_outbox()
- should_insert_message_into_recipients_inbox()
-
when_marking_inbox_message_as_read.cs
-
should_set_IsNew_to_false_if_message_exists_
for_ownerid_and_messageid_combination() -
should_disregard_updating_the_message_if_message_
does_not_exist_for_ownerid_and_messageid_combination()
-
should_set_IsNew_to_false_if_message_exists_
-
when_deleting_inbox_message.cs
-
should_delete_message_if_message_exists_
for_ownerid_and_messageid_combination() -
should_disregard_deletion_of_message_if_message_
does_not_exist_for_ownerid_and_messageid_combination()
-
should_delete_message_if_message_exists_
-
when_deleting_outbox_message.cs
-
should_delete_message_if_message_exists_
for_ownerid_and_messageid_combination() -
should_disregard_deletion_of_message_if_message_
does_not_exist_for_ownerid_and_messageid_combination()
-
should_delete_message_if_message_exists_
Time to Test...and Mock
Let's look at the scaffolding for when_sending_message.cs.
[TestClass]
public when_sending_message
{
private IMessageManager _messageManager; //this is the class we will test
public when_sending_message()
{
//_messageManager initialization will go here
}
[TestMethod]
public void should_insert_message_into_senders_outbox()
{
}
[TestMethod]
public void should_insert_message_into_recipients_inbox()
{
}
}
After creating the scaffold, we need to initialize _messageManager in the constructor so we can test it. If you recall, the concrete implementation of MessageManager has a few dependencies: IUserInboxSaver, IUserOutboxSaver, IUserInboxRetriever, IUserOutboxRetriever. We do not want to test MessageManager's dependencies. So that means we cannot (and should not) use the concrete implementations of these interfaces. We have to use mocks to orchestrate what the concrete implementation of these repository classes would have done.
Manually Making Your Mocks
I used to manually create my mocks. It was fairly easy and didn't require me to spend time on learning a mocking framework...I don't any more, but I'll let you make that decision for yourself. I'll show how to both manually mock your dependencies and how to mock using a mocking framework (I use Moq....it's awesome).
So the problem....I need to new up a MessageManager, and provide mocks for all the interfaces that this class needs. You can accomplish this by having your unit test class implement each interface MessageManager needs. Take a look.
[TestClass]
public when_sending_message :
IUserInboxSaver,
IUserOutboxSaver,
IUserInboxRetriever,
IUserOutboxRetriever
{
private IMessageManager _messageManager; //this is the class we will test
public when_sending_message()
{
//all the interfaces have been implemented
//and either do nothing or return a default value.
//now that when_sending_message implements
//all the interfaces needed by MessageManager,
//I can now new it up.
//"this" (being the unit test class) is passed in
//for each parameter of messagemanager
_messageManager = new MessageManager(this, this, this, this);
}
[TestMethod]
public void should_insert_message_into_recipients_inbox()
{
}
[TestMethod]
public void should_insert_message_into_senders_outbox()
{
}
#region IUserInboxSaver Members
void IUserInboxSaver.SaveInboxMessage(Guid messageId, Guid senderUserId, Guid recipientUserId, string subject, string body, bool isNew, DateTime sendDate)
{
}
void IUserInboxSaver.SetIsNew(Guid messageId, bool isNew)
{
}
void IUserInboxSaver.DeleteInboxMessage(Guid messageId)
{
}
#endregion
#region IUserOutboxSaver Members
void IUserOutboxSaver.SaveOutboxMessage(Guid messageId, Guid senderUserId, Guid recipientUserId, string subject, string body, DateTime sendDate)
{
}
void IUserOutboxSaver.DeleteOutboxMessage(Guid messageId)
{
}
#endregion
#region IUserInboxRetriever Members
bool IUserInboxRetriever.InboxMessageExists(Guid ownerUserId, Guid messageId)
{
return false;
}
List<IMessage> IUserInboxRetriever.GetNewInboxMessages(Guid ownerUserId)
{
return null;
}
List<IMessage> IUserInboxRetriever.GetReadInboxMessages(Guid ownerUserId)
{
return null;
}
IMessageWithBody IUserInboxRetriever.GetInboxMessage(Guid ownerUserId, Guid messageId)
{
return null;
}
#endregion
#region IUserOutboxRetriever Members
bool IUserOutboxRetriever.OutboxMessageExists(Guid ownerUserId, Guid messageId)
{
return false;
}
List<IMessage> IUserOutboxRetriever.GetOutboxMessages(Guid ownerUserId)
{
return null;
}
IMessageWithBody IUserOutboxRetriever.GetOutboxMessage(Guid ownerUserId, Guid messageId)
{
return null;
}
#endregion
}
Alright. We've newed up MessageManager...now let's flush out the should_insert_message_
into_recipients_inbox() method.
[TestMethod]
public void should_insert_message_into_recipients_inbox()
{
Guid senderId = Guid.NewGuid();
Guid recipient = Guid.NewGuid();
_messageManager.SendMessage(senderId, recipientId, "subject", "body", DateTime.Today);
//make sure a new guid was generated for the message id
Assert.AreNotEqual(Guid.Empty, _lastInboxMessageId);
//make sure that rest of the parameters match up as expected
Assert.AreEqual(senderId, _lastInboxSenderUserId);
Assert.AreEqual(recipientUserId, _lastInboxRecipientUserId);
Assert.AreEqual("subject", _lastInboxSubject);
Assert.AreEqual("body", _lastInboxBody);
Assert.AreEqual(DateTime.Today, _lastInboxSendDate);
//ensure that the inbox message is marked as new
Assert.AreEqual(true, _lastInboxIsNew);
}
[...]
private _lastInboxMessageId;
private _lastInboxSenderUserId;
private _lastInboxRecipientUserId;
private _lastInboxSubject;
private _lastInboxBody;
private _lastInboxIsNew;
private _lastInboxSendDate;
void IUserInboxSaver.SaveInboxMessage(Guid messageId, Guid senderUserId, Guid recipientUserId, string subject, string body, bool isNew, DateTime sendDate)
{
//since we injected this unit test class
//as the IUserInboxSaver, this method will
//be called any time MessageManager calls SaveInboxMessage.
//we need to record what was given as the parameters so we
//can do our assertions. This method essentially
//orchestrates the insertion of the message into the database.
_lastInboxMessageId = messageId;
_lastInboxSenderUserId = senderUserId;
_lastInboxRecipientUserId = recipientUserId;
_lastInboxSubject = subject;
_lastInboxBody = body;
_lastInboxIsNew = isNew;
_lastInboxSendDate = sendDate;
}
The should_insert_message_into_senders_outbox() method would be tested in the same fashion. The mocked method would record the parameters that it was given and the unit test would perform assertions on those parameters.
If you are thinking this is a lot of work to effectively unit test...well then you're right. It just comes down to how much you value stable code (and how much it will cost if you introduce a bug into a production environment). Unit tests are necessary. Period. They are an essential part of software engineering (and we all see software as a science/craft). Thankfully, mocking frameworks (like Moq) will make your life a lot easier.
Making Your Mocks With Moq
Here is how you would new up the MessageManager class using [Moq].
using Moq;
[TestClass]
public class when_sending_message
{
private IMessageManager _messageManager;
private Mock<IUserInboxSaver> _userInboxSaver;
private Mock<IUserOutboxSaver> _userOutboxSaver;
private Mock<IUserInboxRetriever> _userInboxRetriever;
private Mock<IUserOutboxRetriever> _userOutboxRetriever;
public when_sending_message()
{
_userInboxSaver = new Mock<IUserInboxSaver>();
_userOutboxSaver = new Mock<IUserOutboxSaver>();
_userInboxRetriever = new Mock<IUserInboxRetriever>();
_userOutboxRetriever = new Mock<IUserOutboxRetriever>();
_messageManager =
new MessageManager(_userInboxSaver.Object,
_userOutboxSaver.Object,
_userInboxRetriever.Object,
_userOutboxRetriever.Object);
}
[TestMethod]
public void should_insert_message_into_recipients_inbox()
{
}
[TestMethod]
public void should_insert_message_into_senders_outbox()
{
}
}
Notice something? The unit test doesn't need to implement every interface. All you have to do is declare a Mock of each interface and inject the mock into the MessageManager class. Now for the real magic. Lets see what the unit test looks like for should_insert_message_into_
recipients_inbox().
[TestMethod]
public void should_insert_message_into_recipients_inbox()
{
Guid senderId = Guid.NewGuid();
Guid recipientId = Guid.NewGuid();
_messageManager.SendMessage(senderId, recipientId, "subject", "body", DateTime.Today);
//verify that the _userInboxSaver.SaveInboxMessage()
//method was called
_userInboxSaver.Verify(
s => s.SaveInboxMessage(It.IsAny<Guid>(), //message id can be any guid
senderId, //sender id
recipientId, //recipient id
"subject", //subject line
"body", //body of message
true, //should be flagged as a new message
DateTime.Today)); //send date should be today
}
That's it...by calling the Verify method, we can ensure that the parameters passed into SaveInboxMessage were conveyed correctly. We didn't have to manually store the values, or do any number of assertions. Moq does all of that for you. Crazy huh?
Tips
That's pretty much all I can give you to get started with creating effective unit tests. The rest comes down to practice...lots and lots of practice. Here are a few tips to help you along.
- When testing your repository methods, you'll will have to interact with a database. Set up your data in a TestInitialize method and use TestCleanup to remove the rows you inserted. Do not assume that data will exist in your database (always setup and tear down). If you can't execute the test on an empty database, you are asking for your suite to fail.
- Your class names for the unit tests should start with the word "when". For example: when_retrieving_a_customer.cs
-
If you have really complicated scenarios, use can also use the word "and" in your unit
test file name. For example: when_retrieving_a_customer_and_the_customer_is_
currently_in_default.cs - Use a mocking framework. It will save you a lot of time and will help you test code that isn't entirely decoupled.
- Shoot for 100% code coverage on your data access layer and domain model. Then as time allows, start testing your controllers/presenters (they are more volatile, and hopefully fairly thin given a robust domain model).
- Test driven development is very difficult and requires discipline. Start with just making sure that new code is tested (even if you end up writing the unit tests after the feature has been developed). Eventually, you'll find that you can leverage your unit tests to actually develop new functionality.
- Unit testing your repository is a real pain to setup and tear down (especially if your development shop has all of its business logic in stored procs). Start transitioning your data access layer to just being a very lean persistence mechanism and start moving your business logic to your domain model.
- Speed is important, you want to be able to run all of your unit tests very quickly (this is another reason why you should get as much as you possibly can out of the data access layer....your repository classes should just blindly retrieve, insert, update, and delete).
- Start abstracting away concrete implementations behind interfaces. Interfaces are very powerful and will help the maintainability of your code.
- Try not to use static methods. They are harder to test. Using IOC containers (like Unity), for controlling class instantiation.
- Practice. Feel free to email me if you have any questions (ar@amirrajan.net).
Written: 4/13/2010