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

The Automated Testing Continuum - Part 2 (Unit Testing LinQ)

by nixusg 5. August 2008 20:00

This is part two 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 examine how to test code that uses a LinQ datacontext without hitting the database and having to worry about data changes. This is one of our don’ts from the first part of the series “Hit the database”. So without further ado let's dive into an example which is a continuation of the example used in Part 1.

Note: If you don’t already have them the MS sample databases can be found here http://codeplex.com/SqlServerSamples

Example Part 2

Let's quickly catch up with how our sample looks. We have a Business Layer which currently holds our CalculatePrice function, a Web layer which does the user interface and a Test project which tests our “complex” logic in the business layer.

So we want to add a dropdown with a tax rate based on region that can be used to set the rate in our calculations. The data that will populate this dropdown comes from the AdventureWorks database so we will create a LinQ datacontext (DataClasses) in our business layer and drop in the tables we require namely SalesTaxRate and StateProvince.

Unfortunatly LinQ was not designed with testability in mind and hence contrains multiple sealed classes with no constructors including the Table class. Therefore to enable testing we need to use one of a few available workaround to fake the datacontext for testing. Some of the available workarounds include:

  • Mocking the datacontext. Currently the only mocking framework that can do this is TypeMock which comes with a hefty €449.00 price tag which just makes it a little impractical.
  • Using reflection to overwrite the provider and create your own implementation. This becomes overly complex and seems like re-inventing the wheel. See http://blogs.msdn.com/mattwar/archive/2008/05/04/mocks-nix-an-extensible-linq-to-sql-datacontext.aspx for more details
  • Hitting the database and using transactions to prevent altering things.
  • Using the repository pattern in your architecture design. Wrapping the datacontext in repository style offers the ability to pass around an interface and is more extensible in the long run allowing you to do other cool things such as building a caching layer. This is therefore my choice and will be the path we follow in this example.

We would like to implement a function that looks like the following to get the data we need for the tax rates:

/// <summary>
/// Get the tax rates from the database
/// </summary>
/// <returns>A list of locations and their associated tax rates</returns>
public List<KeyValuePair<string, string>> GetTaxRates()
{
      return null;
}

But how do we do this while enabling us to abstract the datacontext? Well firstly we implement an interface that will be used to access the datacontext and then we wrap it nicely in a small access class. These two are shown below:

Datacontext interface [intentionally simple for sake of brevity]:

public interface IDataContext : IDisposable
{
      /// <summary>
      /// Get an entity from the repository
      /// </summary>
      /// <typeparam name="TEntity">The entity type</typeparam>
      /// <returns>IQueryable of the given type</returns>
      IQueryable<TEntity> Repository<TEntity>() where TEntity : class;

      /// <summary>
      /// Deletes the specified entity
      /// </summary>
      /// <typeparam name="TEntity">The entity type</typeparam>
      /// <param name="item">The entity to delete</param>
      void Delete<TEntity>(TEntity item) where TEntity : class;

      /// <summary>
      /// Adds the specified entity
      /// </summary>
      /// <typeparam name="T">The entiry type</typeparam>
      /// <param name="item">The entity to add</param>
      void Insert<TEntity>(TEntity item) where TEntity : class;

      /// <summary>
      /// Commits the changes to the repository
      /// </summary>
      void Commit();
}

Wrapper of the datacontext:

public class DataContextWrapper : IDataContext
{
     private readonly DataContext _context;

     /// <summary>
     /// Constructor for the datacontext wrapper
     /// </summary>
     /// <param name="context">DataContext for the wrapper to use</param>
     public DataContextWrapper(DataContext context)
     {
         _context = context;
     }

     /// <summary>
     /// Get an entity from the repository
     /// </summary>
     /// <typeparam name="TEntity">The entity type</typeparam>
     /// <returns>IQueryable of the given type</returns>
     public IQueryable<TEntity> Repository<TEntity>() where TEntity : class
     {
         ITable table = _context.GetTable(typeof(TEntity));
         return table.Cast<TEntity>();
     }

     /// <summary>
     /// Deletes the specified entity
     /// </summary>
     /// <typeparam name="TEntity">The entity type</typeparam>
     /// <param name="item">The entity to delete</param>
     public void Delete<TEntity>(TEntity item) where TEntity : class
     {
         ITable table = _context.GetTable(typeof(TEntity));
         table.DeleteOnSubmit(item);
     }


     /// <summary>
     /// Adds the specified entity
     /// </summary>
     /// <typeparam name="T">The entiry type</typeparam>
     /// <param name="item">The entity to add</param>
     public void Insert<TEntity>(TEntity item) where TEntity : class
     {
         ITable table = _context.GetTable(typeof(TEntity));
         table.InsertOnSubmit(item);
     }

     /// <summary>
     /// Commits the changes to the repository
     /// </summary>
     public void Commit()
     {
         _context.SubmitChanges();
     }

     public void Dispose()
     {
         //Nothing to do here
     }
}

So we now know the method we will use to access the datacontext so we can go ahead and add a little code to our Business Layer to hook all this functionality together. To do this we create a private IDataContext and two constructors, one default one and one that we can use for testing to pass in a fake datacontext.

IDataContext _datacontext;

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

/// <summary>
/// Constructor which takes in an IDataContext to use
/// </summary>
/// <param name="context"></param>
public Calculations(IDataContext context)
{
    _datacontext = context;
}

Now that we have all that in place we can start thinking about writing our tests. Ok but what data will we query in our tests, well the answer to that is whatever fake data we wish to code up. Firstly however we will need a class that fakes the datacontext which is shown below:

public class InMemoryDataContext : IDataContext
{
     //holds our in memory data
     private readonly List<object> _inMemoryDataStore = new List<object>();

     /// <summary>
     /// Get an entity from the repository
     /// </summary>
     /// <typeparam name="TEntity">The entity type</typeparam>
     /// <returns>IQueryable of the given type</returns>
     public IQueryable<TEntity> Repository<TEntity>() where TEntity : class
     {
          var query = from objects in _inMemoryDataStore
                      where typeof(TEntity).IsAssignableFrom(objects.GetType())
                      select objects;

          return query.Select(o => (TEntity)o).AsQueryable();
     }

     /// <summary>
     /// Deletes the specified entity
     /// </summary>
     /// <typeparam name="TEntity">The entity type</typeparam>
     /// <param name="item">The entity to delete</param>
     public void Insert<TEntity>(TEntity item) where TEntity : class
     {
          _inMemoryDataStore.Add(item);
     }

     /// <summary>
     /// Adds the specified entity
     /// </summary>
     /// <typeparam name="T">The entiry type</typeparam>
     /// <param name="item">The entity to add</param>
     public void Delete<TEntity>(TEntity item) where TEntity : class
     {
          _inMemoryDataStore.Remove(item);
     }

     /// <summary>
     /// Commits the changes to the repository
     /// </summary>
     public void Commit()
     {
         //Nothing to do here
     }

     #region IDisposable Members

     public void Dispose()
     {
         //Nothing to do here
     }
     #endregion
}

We are now ready to add data to our fake datacontext, we do this 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};

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

      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};

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

      return testContext;
}

Now after that lengthly but unfortunately necessary process we can write our test. Of course you only need to do all this setup once after which you can use it with multiple tests. So let’s implement that test which of course we expect to fail because we don’t actually do the query yet but just return null.

/// <summary>
///A test for GetTaxRates
///</summary>
[TestMethod()]
public void GetTaxRatesTest()
{
Calculations target = new Calculations(SetupDataContext());
      List<KeyValuePair<string, string>> actual;
      int expectedCount;
      actual = target.GetTaxRates();
      expectedCount = 2;
      Assert.AreEqual(expectedCount, actual.Count);
}

So there we have it a simple test of our function that as we expected fails throwing an exception which is great as it is one of our don’ts “Handle exceptions (we want these thrown)”.

Let’s go ahead and write that function to return the correct data. It turns out as follows:

/// <summary>
/// Get the tax rates from the database
/// </summary>
/// <returns>A list of locations and their associated
    tax rates</returns>
public List<KeyValuePair<string, string>> GetTaxRates()
{
       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
        select new KeyValuePair<string, string>(stateProvince.Name, salesTax.TaxRate.ToString())
        ).ToList();

        return rates;
}

So we run the test again and it passes great :)

All that is left is to hook this all up to the presentation layer and then we will have a working sample. We modify the default.aspx page as follows to get this all working:

Default.aspx:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    Tax Region: 
        <asp:DropDownList ID="regionRateDropDownList" runat="server">
        </asp:DropDownList>
    </div>
    <div>
        <asp:TextBox ID="costTextBox" runat="server"></asp:TextBox>
        <asp:Button ID="calculateButton" runat="server" Text="Calculate" 
            onclick="calculateButton_Click" />
    </div>
    <div>
        Total Calculated Value:<asp:Label ID="PriceLabel" runat="server" Text=""></asp:Label>
    </div>
    </form>
</body>
</html>

Default.aspx.cs:

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            PopulateRegionRates();
        }
    }

    private void PopulateRegionRates()
    {
         Calculations calcs = new Calculations();
         regionRateDropDownList.DataSource = calcs.GetTaxRates();
         regionRateDropDownList.DataTextField = "Key";
         regionRateDropDownList.DataValueField = "Value";
         regionRateDropDownList.DataBind();

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

    protected void calculateButton_Click(object sender, EventArgs e)
    {
        decimal cost = 0;
        decimal rate = 0;
            
        if (decimal.TryParse(costTextBox.Text, out cost) && 
                decimal.TryParse(regionRateDropDownList.SelectedValue, out rate))
        {
            PriceLabel.Text = Calculations.CalculatePrice(cost, rate).ToString();
        }
    }

}

After this we end up with a page which allows us to select a region and calculate the full price of an item including tax.

There you have it a fully testable solution implemented with LinQ.

Come back soon for the next part in the series which will deal with mocking a webservice for testing using the Moq mocking framework. We will filter the list of regions based on the user’s IP address to a likely set of regions.

You can download the full source code TestingContinuumPart2.zip (31.67 kb).

Currently rated 4.7 by 3 people

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

Tags: , , ,

Unit Testing

The Automated Testing Continuum - Part1 (Unit Testing)

by nixusg 5. August 2008 19:29

This is part one 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. These posts will not only focus on developer tools but also approach the problem from other disciplines.

Some of the tools that will be covered are Unit tests (MSUnit), mocking (MoQ), interface testing via code (WaitN), fitnesse testing via code and English (FitNesse), user interface testing via English (WebFixture). Along with whatever I come up with in between.

Now with the introductions done I will dive into the topic of the first part namely Unit testing. A unit test is a small piece of code that tests whether the logic of a more complex piece of code is functioning correctly. Some of the do’s and don’ts of unit tests are:

Do:

  • Run Fast
  • Test fragile or complicated code (use Code metrics to determine complexity)
  • Test difficult to spot errors (rounding etc)
Don't:
  • Test private methods
  • Hit the database
  • Hit external interfaces
  • Handle exceptions (we want these thrown)
  • Test application performance

But enough rambling let’s take a look at a concrete example:

The sample application we will be building and testing throughout this series will be a simple webpage that calculates the tax cost on an item based on where you live.

Example Part 1

To start this off we will write a simple function to calculate the tax given the tax rate and the items cost. So first off we need a solution with an ASP.Net web application and a business layer class library.

We will need to add a class Calculations to the business layer and create our CalculatePrice function with a simple return -1 statement.

public class Calculations
{
      public static decimal CalculatePrice(decimal price, decimal tax)
      {
          return -1;
      }
}

Ok so now we have our method let’s write a test for it (which we expect to fail at this point). Right click on the method and select “Create Unit Tests...” which will bring up a selection of tests the default options are fine so we click ok and our test project is generated with a default test great!

/// <summary>
///A test for CalculatePrice
///</summary>
 [TestMethod()]
public void CalculatePriceTest()
{
      Decimal price = new Decimal(); // TODO: Initialize to an appropriate value
      Decimal tax = new Decimal(); // TODO: Initialize to an appropriate value
      Decimal expected = new Decimal(); // TODO: Initialize to an appropriate value
      Decimal actual;
      actual = Calculations.CalculatePrice(price, tax);
      Assert.AreEqual(expected, actual);
      Assert.Inconclusive("Verify the correctness of this test method.");
}

Ok so this does not actually test our logic properly so let’s modify it a bit based on our expectations and see how things go. Remember our do's and don’ts? One of the do's states “Test difficult to spot errors (rounding etc)” so let’s go ahead and do this:

/// <summary>
///A test for CalculatePrice
///</summary>
[TestMethod()]
public void CalculatePriceTest()
{
      Decimal price = 100.999M;
      Decimal tax = 13.993M;
      Decimal expected = 115.13M;
      Decimal actual;
      actual = Calculations.CalculatePrice(price, tax);
      Assert.AreEqual(expected, actual);
}

Now we have a test for our function let's run it (right click run tests) and see what happens.

So it fails but that is exactly what we were expecting seeing as we are just returning -1. Now let’s actually implement the method properly and try that again.

public static decimal CalculatePrice(decimal price, decimal tax)
{
      return decimal.Round(price * ((tax / 100) + 1), 2);
}

Running the test now it passes yay!

So in terms of testing principles we have just gone from red to green, if it were needed we could now refractor without worrying about breaking our function as we can simply test if the refractor broke anything.

Now that the Business Layer is implemented we can quickly add the frontend by modifying our default.aspx page as follows:

Default.aspx:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="costTextBox" runat="server"></asp:TextBox>
        <asp:Button ID="calculateButton" runat="server" Text="Calculate" 
            onclick="calculateButton_Click" />
    </div>
    <div>
        Total Calculated Value:<asp:Label ID="PriceLabel" runat="server" Text=""></asp:Label>
    </div>
    </form>
</body>
</html>

Default.aspx.cs:

public partial class _Default : System.Web.UI.Page
{
      protected void calculateButton_Click(object sender, EventArgs e)
      {
          decimal cost = 0;
            
          if (decimal.TryParse(costTextBox.Text, out cost))
          {
              PriceLabel.Text = Calculations.CalculatePrice(cost, 14).ToString();
          }
      }
}

Browsing to this page we can now calculate the total cost of an item for the hardcoded tax rate.

Download the example code TestingContinuumPart1.zip (29.46 kb)

So now that we are all up to speed on writing simple unit tests and their uses we can look forward to part 2 where I will add a dropdown that is populated from the AdventureWorks database using LinQ to select the tax rate. This will demonstrate how to unit test without hitting the database and some other general principals.

Hope you enjoyed this article please come back to see the next post in the series later.

Currently rated 4.0 by 1 people

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

Tags: , ,

Unit Testing

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen