The Automated Testing Continuum - Part 3 (Moq)

by nixusg 10. August 2008 13:55

This is part three in a series that will cover various tools, and the theory behind them, that can be used to automate the testing process and hence improve software quality.

In this part we will investigate mocking and how it can be used to ensure that our tests run fast and do not hit external interfaces. This will cover the do “Run Fast” and the don’t “Hit external interfaces” that were mentioned in Part 1 of this series.

We will use a webservice to filter the list of regions based on the user’s IP address to a likely set of regions and test this piece of code by mocking the webservice used. To do this we will use a free webservice provided by WebserviceX.NET http://www.webservicex.net

So with that let’s dive into our example.

Example Part 3

Let’s quickly catch up with how our sample looks we have a Business Layer which currently holds our CalculatePrice function, the GetTaxRates function and uses the repository pattern to access the database using LINQ. We have a Web layer which does the user interface and a Test project which tests our “complex” logic in the business layer and fakes the LINQ database.

So we want to filter the list of available regions based on the user’s country but how do we do this? The first step is to translate the users IP address, which we can get from the Request object using Request.UserHostAddress, into a country name by sending it off to a webservice provided by WebserviceX.NET. To use this webservice we add a service reference to our business layer pointing to http://www.webservicex.net/geoipservice.asmx?WSDL. This gives us access to various methods and objects of which the GeoIPServiceSoap interface, GeoIP class, GeoIPServiceSoapClient class and the GetGeoIP method of that class are important to us.

Ordinarily I would write the test first and do the implementation later as per proper TDD principals but for ease of explanation I will do part of the implementation first this time. So initially we will setup our private Soap Client in the Calculations class to access the webservice and modify our constructors to set it up correctly. The reason we set things up like this is to make it easier for us to test later by mocking the Soap Client but more on that later.

IDataContext _datacontext;
GeoIPServiceSoap _soapClient;

/// <summary>
/// Constructor which initializes the default datacontext
/// </summary>
public Calculations()
{
      _datacontext = new DataContextWrapper(new DataClassesDataContext());
      _soapClient = new GeoIPServiceSoapClient();
}

/// <summary>
/// Constructor which takes in an IDataContext and GeoIPServiceSoap
    to use for testing
/// </summary>
/// <param name="context"></param>
public Calculations(IDataContext context, GeoIPServiceSoap geoSoapClient)
{
      _datacontext = context;
      _soapClient = geoSoapClient;
}

Before we get into the tests we will also modify our GetTaxRates function to take in the user’s IP address but not do anything with it yet as follows.

/// <summary>
/// Get the tax rates from the database
/// </summary>
/// <param name="ipAddress">The users ip address to filter the list</param>
/// <returns>A list of locations and their associated tax rates</returns>
public List<KeyValuePair<string, string>> GetTaxRates(string ipAddress)
{
      List<KeyValuePair<string, string>> rates =
      (
          from stateProvince in _datacontext.Repository<StateProvince>()
          from salesTax in _datacontext.Repository<SalesTaxRate>()
          where stateProvince.StateProvinceID == salesTax.StateProvinceID
                && salesTax.TaxRate != null
                && (salesTax.TaxType == 1 || salesTax.TaxType == 3)
          select new KeyValuePair<string, string>(stateProvince.Name, salesTax.TaxRate.ToString())
      ).ToList();

      return rates;
}

Now that we have the first part of the implementation out of the way we can write our tests and then make the necessary changes to the GetTaxRates function to do the correct filtering.

Looking at the in memory database we setup in the last part we do not have sufficient data to actually test the modifications we would like to make so we are going to alter our SetupDataContext function to contain a second country as follows:

/// <summary>
/// Setup the DataContext for use in tests
/// </summary>
/// <returns>IDataContext with in memory data</returns>
private IDataContext SetupDataContext()
{
      InMemoryDataContext testContext = new InMemoryDataContext();

      StateProvince stateProvince1 = new StateProvince { CountryRegionCode="US", 
          IsOnlyStateProvinceFlag=true, ModifiedDate=DateTime.Now, Name="Alabama", 
          rowguid=Guid.NewGuid(), SalesTaxRates=null, StateProvinceCode="AL", StateProvinceID=1, 
          TerritoryID=1};
      StateProvince stateProvince2 = new StateProvince { CountryRegionCode="US", 
          IsOnlyStateProvinceFlag=true, ModifiedDate=DateTime.Now, Name="Indiana", 
          rowguid=Guid.NewGuid(), SalesTaxRates=null, StateProvinceCode="IN", StateProvinceID=2, 
          TerritoryID=1};
      StateProvince stateProvince3 = new StateProvince { CountryRegionCode="US", 
          IsOnlyStateProvinceFlag=true, ModifiedDate=DateTime.Now, Name="Ohio", 
          rowguid=Guid.NewGuid(), SalesTaxRates=null, StateProvinceCode="OH", StateProvinceID=3, 
          TerritoryID=1};
      StateProvince stateProvince4 = new StateProvince { CountryRegionCode="ZA", 
          IsOnlyStateProvinceFlag=true, ModifiedDate=DateTime.Now, Name="Western Cape", 
          rowguid=Guid.NewGuid(), SalesTaxRates=null, StateProvinceCode="WC", StateProvinceID=4, 
          TerritoryID=1};

      testContext.Insert(stateProvince1); 
      testContext.Insert(stateProvince2); 
      testContext.Insert(stateProvince3);
      testContext.Insert(stateProvince4);


      SalesTaxRate salesTax1 = new SalesTaxRate { ModifiedDate=DateTime.Now, Name="Alabama", 
          rowguid=Guid.NewGuid(), SalesTaxRateID=1, StateProvince=stateProvince1, 
          StateProvinceID=1, TaxRate=7.5M, TaxType=1};
      SalesTaxRate salesTax2 = new SalesTaxRate { ModifiedDate=DateTime.Now, Name="Indiana", 
          rowguid=Guid.NewGuid(), SalesTaxRateID=2, StateProvince=stateProvince2, 
          StateProvinceID=2, TaxRate=7.8M, TaxType=1};
      SalesTaxRate salesTax3 = new SalesTaxRate { ModifiedDate=DateTime.Now, Name="Western Cape", 
          rowguid=Guid.NewGuid(), SalesTaxRateID=2, StateProvince=stateProvince4, 
          StateProvinceID=4, TaxRate=14.0M, TaxType=1 };

      testContext.Insert(salesTax1); 
      testContext.Insert(salesTax2);
      testContext.Insert(salesTax3); 

      return testContext;
}

Now that we have proper test data we can go ahead and implement our tests. In our tests we don’t actually want to call the webservice as it is an external interface and potentially runs slowly. This is where mocking comes into play. A mock object is a simulated object that mimics the behaviour of a real object in a controlled way. This is done in much the same way that a car designer uses crash test dummies instead of real people.

Moq has been chosen as our mocking framework due to its C# 3.0 integration and ease of use. Moq can be downloaded at http://code.google.com/p/moq/. We add the Moq.dll as a reference to our test project giving us access to the rich mocking functionality.

We now have the functionality we need but how exactly do we mock objects? Well Moq can create mock objects of both interfaces and classes using the simple syntax below.

var soapClient = new Moq.Mock<GeoIPServiceSoap>(); 

Here we are creating a mock of the GeoIPServiceSoap interface but currently it does not do anything. The Mock class is a class from the Moq framework and has a generic constructor that accepts the type of the interface to create. To create canned responses for our mock object we use lambda expressions to represent properties and methods and the Returns property to specify the return value. So to create a canned response for the GetGeoIP function accepting the IP address “127.0.0.1” and returning the country code “US” we write the following line of code.

soapClient.Expect(client => client.GetGeoIP("127.0.0.1")).Returns(new GeoIP { 
                                                                          CountryCode = "US" });

Moq has much greater functionality than this simple example but that is outside the scope of this article. To read more about the power of Moq see http://code.google.com/p/moq/ and http://weblogs.asp.net/stephenwalther/archive/2008/06/11/tdd-introduction-to-moq.aspx

With that small bit of the theory, and use of mocking, behind us we will go ahead and write our test to test the GetTaxRates function without using the real webservice. The test ends up looking like the following.

/// <summary>
///A test for GetTaxRates
///</summary>
[TestMethod()]
public void GetTaxRatesTest()
{
      var soapClient = new Moq.Mock<GeoIPServiceSoap>();
      soapClient.Expect(client => client.GetGeoIP("127.0.0.1")).Returns(new GeoIP { 
                                                                          CountryCode = "US" });

      Calculations target = new Calculations(SetupDataContext(), soapClient.Object);
            
      List<KeyValuePair<string, string>> actual;
      int expectedCount;
      actual = target.GetTaxRates("127.0.0.1");
      expectedCount = 2;
      Assert.AreEqual(expectedCount, actual.Count);
}

Firstly we mock out the webservice then we create an instance of the Calculations class using the testing constructor passing in our in-memory database and our mock webservice. We expect that this filtered list will return only the two regions in the US but upon running the test it fails because we have not implemented the filtering yet. We are currently in the red state lets implement the filtering and move towards green.

Implementing our filtering in the Calculations class we end up with the following:

/// <summary>
/// Get the tax rates from the database
/// </summary>
/// <param name="ipAddress">The users ip address to filter the list</param>
/// <returns>A list of locations and their associated tax rates</returns>
public List<KeyValuePair<string, string>> GetTaxRates(string ipAddress)
{
      string country = _soapClient.GetGeoIP(ipAddress).CountryCode;

      List<KeyValuePair<string, string>> rates =
      (
            from stateProvince in _datacontext.Repository<StateProvince>()
            from salesTax in _datacontext.Repository<SalesTaxRate>()
            where stateProvince.StateProvinceID == salesTax.StateProvinceID
                  && salesTax.TaxRate != null
                  && (salesTax.TaxType == 1 || salesTax.TaxType == 3)
                  && stateProvince.CountryRegionCode == country
            select new KeyValuePair<string, string>(stateProvince.Name, 
                                                    salesTax.TaxRate.ToString())
      ).ToList();

      return rates;
}

Running our test again we note that everything passes and we are in the green state. We achieved all of this without even calling the real webservice!

All that is left now is to modify the PopulateRegionRates method in Default.aspx.cs to filter the list correctly.

/// <summary>
/// Populate the region doropdown list
/// </summary>
private void PopulateRegionRates()
{
      Calculations calcs = new Calculations();

      regionRateDropDownList.DataSource = calcs.GetTaxRates(Request.UserHostAddress);

      regionRateDropDownList.DataTextField = "Key";
      regionRateDropDownList.DataValueField = "Value";
      regionRateDropDownList.DataBind();

      regionRateDropDownList.Items.Insert(0, new ListItem("<--Select One-->", ""));
}

All our functionality is now implemented so faking an external IP address by either hard coding it or using a proxy we open the final webpage and see if we get what we expect.

Faking a German IP address it turns out that everything is working correctly! So there we have it a very brief introduction to mocking and testing an external webservice interface using Moq.

The full source code for this sample can be downloaded TestingContinuumPart3.zip (119.52 kb).

Come back soon for the next part in the series which will deal with testing the application functionality using WaitN.

Currently rated 4.0 by 1 people

  • Currently 4/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: , , , ,

Unit Testing

Comments are closed

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen