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

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen