Thursday, October 27, 2011

Unit testing with mock objects

This article is meant as a short tutorial on how to use an isolation framework. I prefer Moq for my .NET projects, but I'm sure there are other good alternatives out there. Moq can be pulled from NuGet. Simply add it to your test project and you're good to go.

When do we use mocking? The answer is, we use mocking when we need to isolate a unit for testing. Let's say I have a method that pulls a resource from the internet, looks at it and returns a list of strings. What we want to test is how different inputs gives different results, not the actual operation of opening the web page and streaming data.

I have some code ready and I will demonstrate how to write some tests for it. In the code below, I download and go through an xml document and look for nodes containing names:
public List<string> FetchData(string url)
{
    // Get XmlDocument using WebClient
    var document = new XmlDocument();
    document.Load(new WebClient().OpenRead(url));
           
    // Go through the document and aggregate a list of strings
    var list = new List<string>();
    foreach (var node in document.GetElementsByTagName("name"))
    {
        list.Add(((XmlNode) node).InnerText);
    }

    return list;
}


Separate concerns

Now, the first thing I have to do is to take out the code that talks to the internet. This way, I can inject different result data and see how it affects the output. I simply set up a wrapper for the WebClient, so that the rest of the code can be isolated for testing. Here's my wrapper class:
public interface IWebClientWrapper
{
    XmlDocument FetchXml(string url);
}

Here's an implementation of my interface:
public class WebClientWrapper : IWebClientWrapper
{
    public XmlDocument FetchXml(string url)
    {
        // Do stuff and return XmlDocument
    }
}

Now, I have a separate unit that speeks with the internet and a separate unit that interprets the results. I've set up the wrapper class with an interface so that it can be mocked for testing. Either that or its methods would have to be marked as virtual.


Prep the code

Next, in my web-fetch-thing-class, I set up a new property, WebClientWrapper. The property has the type IWebClientProxy. If it is not set, it will simply create a new instance. This pattern allows me to inject a custom instance of IWebClientProxy for testing.

private IWebClientWrapper _webClientWrapper;
public IWebClientWrapper WebClientWrapper
{
    get
    {
        if (_webClientWrapper == null)
            _webClientWrapper = new WebClientWrapper();
        return _webClientWrapper;
    }
    set { _webClientWrapper = value; }
}

My FetchData method now looks like this:
public List<string> FetchData(string url)
{
    // Get XmlDocument from the WebClient wrapper class
    var document = WebClientWrapper.FetchXml(url);

    // Go through the document and aggregate a list of strings
    var list = new List<string>();
    foreach (var node in document.GetElementsByTagName("name"))
    {
        list.Add(((XmlNode) node).InnerText);
    }

    return list;
}

Testing the code

Let's take a look at the actual tests. I wanted to test how different results affects how my method aggregates a list of names. Let's start with some code:
var webClientWrapper = new Mock<IWebClientWrapper>();
webClientWrapper
   .Setup(x => x.FetchXml(It.IsAny<string>()))
   .Returns((XmlDocument)null);

In the snippet above, we've created a new instance of a Mock class of the type IWebClientWrapper. We've set up the method FetchXml(...) to take any input, so long as it is of the type String and we've stated that it should return null. Note how we have to do casting when we want to return null. This is because the Return(...) method has overloads, and we have to explicitly tell which one we are using.

Next, have a look at how we inject the mock object. Remember how I made the WebClient wrapper into a property?
var webFetcher = new WebFetcher 
                 { 
                    WebClientWrapper = webClientWrapper.Object 
                 };

Now, I can excute my method, and see how it behaves. I actually want it to throw an exception if the WebClient returns null. Have a look at the full test:
[TestMethod]
[ExpectedException(typeof(Exception), AllowDerivedTypes = true)]
public void WebFetcher_TestWithNullDocument_ShouldThrowException()
{
    // Setup
    var webClientWrapper = new Mock<IWebClientWrapper>();
    webClientWrapper.Setup(x => x.FetchXml(It.IsAny<string>())).Returns((XmlDocument)null);

    var webFetcher = new WebFetcher 
                     { 
                        WebClientWrapper = webClientWrapper.Object 
                     };

    // Execute
    webFetcher.FetchData("http://www.somewhere.com/validUrl/");
}

I also want to see how the results are when we provide a valid xml document. Have a look at the following test:
[TestMethod]
public void WebFetcher_TestWithValidExampleData_ShouldReturnXmlDocument()
{
    // Setup
    var document = new XmlDocument();
    document.LoadXml("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><names><name>Brian</name><name>John</name></names>");

    var webClientWrapper = new Mock<IWebClientWrapper>();
    webClientWrapper.Setup(x => x.FetchXml(It.IsAny<string>())).Returns(document);

    var webFetcher = new WebFetcher 
                     { 
                        WebClientWrapper = webClientWrapper.Object 
                     };

    // Execute
    var list = webFetcher.FetchData("http://www.somewhere.com/validUrl/");

    // Assert
    Assert.IsNotNull(list);
    Assert.IsInstanceOfType(list, typeof (List));
    Assert.IsTrue(list.Count != 0);
    Assert.IsTrue(list.Contains("Brian"));
    Assert.IsTrue(list.Contains("John"));
}

I will have to make some changes in my original code. At least now, It's more testable and I can see how different input data affects the output.

The code is posted on Bitbucket if you want to have a look:
https://bitbucket.org/andersnygaard/webfetcherthingy/src

5 comments:

Fraz said...

Nice post, thanks for taking the time to write it. I have one question. I see your fetchData() returns a list. If you had a another method in the same class which calls the fetchData(), how would u structure the test for that method + how would u mock fetchData()?

Technofide said...

Thanks for the post. Really helpful.

Siva said...

private IWebClientWrapper _webClientWrapper; public IWebClientWrapper WebClientWrapper { get { if (_webClientWrapper == null) _webClientWrapper = new WebClientWrapper(); return _webClientWrapper; } set { _webClientWrapper = value; } }

so this was created specially for testing purpose?

Siva said...
This comment has been removed by the author.
Samuel Robson-Davis said...

iwebclientproxy does not seem to be used - was this supposed to be the type?