Software QA FYI - SQAFYI

Unit Testing in .NET Part 5 - Moq-ing your way to simpler tests

By:

Previously on "Unit Testing in .NET", we've looked at:
* Introduction to Testing
* Your First Unit Tests
* Asserting That Your Code Rocks
* Overcoming Testing Hurdles


That means you should be comfortable with the all the fundamental concepts of software testing, and you should be equipped to deal with some common testing headaches. In this post, we're going to look at how to simplify testing via dependency injection and the excellent mocking library, Moq.

What is mocking?
Mocking refers to the act of substituting a simulated object in place of a real object. In unit testing, it is quite common to use mocking to separate the class-under-test from its external dependencies, making the class much easier to fully test. I didn't talk much about it, but that's exactly what we were doing in the previous post when we applied dependency injection and the adapter pattern. We were substituting a fake object (the mock) that we had given a specific behavior to for the real object.

Sidetrack: what's the difference between a mock object, a fake object, and a stub?
Nothing that really matters, in my opinion. Purists will argue the semantic differences between them, but I don't think that's really helpful as long as you use a mocking framework that is flexible enough to meet your testing needs.

Moq
Moq (I prefer the "mock" pronunciation just because it's only one syllable instead of two) is a newer mocking framework for .NET that leverages many of the new platform and language features that arrived in .NET 3.5. This gives it an advantage over older mocking frameworks that were stuck using the features that were available in previous versions of .NET.

Getting started with Moq is easy. Just grab the DLL, add a reference to it, and start coding. Note that we will be using Moq 3.0 Beta, and that there are some major changes from 2.6 to 3.0, so the code below may need to be modified somewhat if you are using an older version.

Our First Test
Let's revisit our LogMonitor from the previous post. Recall that this class takes in a log message, then dispatches an E-mail if the message contains the string "error". Originally, the class was tied directly to SmtpClient, but we applied the adapter pattern and dependency injection to simplify the tests. Here's the original LogMonitor code:
1: /// <summary>
2: /// Inspects log messages and sends E-mails if an error occurs.
3: /// <summary>
4: public class LogMonitor
5: {
6: /// <summary>
7: /// The client used to send E-mail
8: /// </summary>
9: private ISmtpClient mClient;
10:
11: /// <summary>
12: /// Creates a monitor that will use the default SMTP client.
13: /// </summary>
14: public LogMonitor() : this(new SmtpClientAdapter())
15: {
16:
17: }
18:
19: /// <summary>
20: /// Creates a monitor that will use the specified
21: /// <see cref="ISmtpClient"/>.
22: /// </summary>
23: /// <param name="smtpClient"></param>
24: public LogMonitor(ISmtpClient smtpClient)
25: {
26: mClient = smtpClient;
27: }
28:
29: /// <summary>
30: /// Inspects log messages, and sends an E-mail if a message
31: /// contains the word "error".
32: /// </summary>
33: /// <param name="message"></param>
34: public void InspectLogMessage(string message)
35: {
36: if (message.ToUpper().Contains("ERROR"))
37: {
38: mClient.Send("system@somewhere.com", "support@nospam.com", "Error!", message);
39: }
40: }
41: }
42:
43: /// <summary>
44: /// Wrapper interface for .
45: /// </summary>
46: public interface ISmtpClient
47: {
48: /// <summary>
49: /// Sends an E-mail.
50: /// </summary>
51: /// <param name="from"></param>
52: /// <param name="recipients"></param>
53: /// <param name="subject"></param>
54: /// <param name="body"></param>
55: void Send(string from, string recipients, string subject, string body);
56: }
57:
58: /// <summary>
59: /// Adapts <see cref="SmtpClient"/> into the <see cref="ISmtpClient"/>
60: /// interface.
61: /// </summary>
62: public class SmtpClientAdapter : ISmtpClient
63: {
64: /// <summary>
65: /// Sends an E-mail.
66: /// </summary>
67: /// <param name="from"></param>
68: /// <param name="recipients"></param>
69: /// <param name="subject"></param>
70: /// <param name="body"></param>
71: public void Send(string from, string recipients, string subject, string body)
72: {
73: SmtpClient client = new SmtpClient();
74: client.Send(from, recipients, subject, body);
75: }
76: }
And the LogMonitor tests:
1: /// <summary>
2: /// Tests fixture for <see cref="LogMonitor"/>.
3: /// </summary>
4: [TestFixture]
5: public class LogMonitorTests
6: {
7: ///


8: /// Test class.
9: /// </summary>
10: private class TestSmtpClient : ISmtpClient
11: {
12: /// <summary>
13: /// Incremented every time a message is sent.
14: /// </summary>
15: public int MessagesSent { get; private set; }
16:
17: /// <summary>
18: /// Records that a message was sent.
19: /// </summary>
20: /// <param name="from"></param>
21: /// <param name="recipients"></param>
22: /// <param name="subject"></param>
23: /// <param name="body"></param>
24: public void Send(string from, string recipients, string subject, string body)
25: {
26: Console.WriteLine("Received message: {0}", body);
27: MessagesSent++;
28: }
29: }
30:
31: /// <summary>
32: /// Verifies that the method correctly sends an E-mail
33: /// when it receives an error log.
34: /// </summary>
35: [Test]
36: public void InspectLogMessage_SendsErrorEmail()
37: {
38: TestSmtpClient client = new TestSmtpClient();
39:
40: LogMonitor monitor = new LogMonitor(client);
41:
42: monitor.InspectLogMessage("This is an error.");
43:
44: Assert.AreEqual(1, client.MessagesSent);
45: }
46: }

Notice that we had to create a test class to serve as the mock object for LogMonitor. All is well. But wait, the requirements are about to change! Now we need to extend LogMonitor to handle warning messages, and warning and error messages need to go to two different accounts. Well, we *could* modify our test class to track the E-mails that were sent, then verify that the correct E-mail was sent for each log message we pass in, but then what's going to happen when they want to add support for "fatal" log messages? Or super-critical-urgent messages? Our simple test class is going to become complicated in a hurry.

Enter Moq. Moq let's us get rid of the test class for ISmtpClient completely. Instead, we'll let Moq dynamically create a mock object for us. All we have to do is setup the expectations, the actions we expect the mock object to perform in response to certain method calls. In Moq 2.x, you did this using the Expects method, but in Moq 3.0, you use the new Setup method. For our first test, let's verify that error messages are being sent to the new address by using Moq:

1: /// <summary>
2: /// Verifies that the method correctly sends an E-mail
3: /// when it receives an error log.
4: /// </summary>
5: [Test]
6: public void InspectLogMessage_SendsErrorEmail()
7: {
8: var mock = new Mock<ISmtpClient>(MockBehavior.Strict);
9:
10: mock.Setup(c => c.Send("errors@somewhere.com", "support@nospam.com", "Error!", It.IsAny())).AtMostOnce();
11:
12: LogMonitor monitor = new LogMonitor(mock.Object);
13:
14: monitor.InspectLogMessage("This is an error.");
15:
16: mock.Verify(c => c.Send("errors@somewhere.com", "support@nospam.com", "Error!", It.IsAny<string>()));
17: }

There are a few important things to call out here. First, we are creating a strict mock, which means that any calls to the mock which aren't configured using the Setup method will result in exceptions. Note how we setup our expectations by specifying the exact values we expect for most of the parameters, but we use It.IsAny for the last parameter. That says "match any string value" for the last argument.

Second, the mock reference we create can't be passed directly in as an ISmtpClient. You might think that Moq could provide an automatic cast or something, but the .NET framework just won't allow it in the current version. So, to get to the actual mocked object, you access the Object property. Simply pass mock.Object in to LogMonitor just as you would any class that implements ISmtpClient.

Finally, notice that we have a call to Verify at the end. This will throw an exception if the exact call specified wasn't executed. It is important to make sure that this matches the call you specified to Setup, otherwise your test will fail.

Go ahead and run the test. It should fail because we haven't changed the address that error E-mails are being sent to. Do that now by editing LogMonitor, replacing "system@somewhere.com" with "errors@somewhere.com". Re-run the test, and it should pass.

Next, let's add a test for the warning messages. It looks almost identical to the test for errors:

Full article...


Other Resource

... to read more articles, visit http://sqa.fyicenter.com/art/

Unit Testing in .NET Part 5 - Moq-ing your way to simpler tests