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.