How To Unit Test – Using SubSonic

In my previous article I wrote about how to unit test with the database.  However, SubSonic can do all of that work for us.  If you haven’t used it before, SubSonic is a great Object Relational Mapping Framework based on ActiveRecord (same as Ruby on Rails) for .Net 2.0. Simply point it at a database and it will create your all the code for connecting to your database. But once you have this code, how can you test with it?

However, just because SubSonic is a nature and well tested framework it doesn’t mean that you can trust the code it creates as it still needs to be tested.  But don’t expect 100% code coverage!  You will still want to test that the parts of subsonic you are using are actually working correctly. You shouldn’t find any errors, but you ever known!

Scenario

In this post I’m going to create a simple application to insert customers into our database via a console application and also have a list of customers printed out to the console. Similar to the previous post.

The application will be separated into three parts. SubSonic will be used for all the database access, a middle layer will be the ‘glue’ and perform validation on the input (this could be business rules) and will sit on top of subsonic classes.  This will then be accessible from the console application (but it shouldn’t take much to change this to a WinForm/WebForm application).  I know it’s simple (and a bit uninteresting) but I just want to demonstrate the concepts more than anything else.

Unit Tests

The first thing to do is create the database and generate the classes for the database.  I spoke about how to setup SubSonic in a previous post.

Next, I want to be able to insert a customer.

[Test]
public void CreateCustomer()
{
    string name = “John Smith”;
    string email = “JohnSmith@GoogleMail.com”;
    Customer c = CustomerManager.CreateCustomer(name, email);
    Assert.AreEqual(name, c.Name);
    Assert.AreEqual(email, c.Email);
}

Here, I have created a CustomerManager static class to provider a layer above the SubSonic implementation. 

I then return a SubSonic generated Customer object.  This is actually really bad as we are returning an object of type ActiveRecord with a lot of additional methods which we wouldn’t want other layers to access (like .Save()).  By doing this, we also need to reference the SubSonic assembly within our test framework, again this is really bad if you want a good separation of the system as we have become dependent on SubSonic.  To stop this, i’m actually going to return our own Customer object to high-layers with just the information we want to expose, we lose some of the advantages of SubSonic but I can accept that for the abstraction. For the moment, it will be just Name and Email.

[Test]
public void CreateCustomer()
{
    string name =  “Customer Test”;
    string email = “Customer@Test.com”;
    Customer c = new Customer(name, email);
    Assert.AreEqual(name, c.Name);
    Assert.AreEqual(email, c.Email);
}

Our CustomerManager implementation then looks like this:

public static Customer CreateCustomer(string name, string email)
{
    HowToUnitTestDB.Customer c = new HowToUnitTestDB.Customer();
    c.Name = name;
    c.Email = email;
    c.Save();

    return new Customer(c.Name, c.Email);
}

Here we are creating two objects, but I feel a lot more conformable with this level of expose and flexibility. If we change to Linq to SQL tomorrow, our higher layers won’t care.

Back to the Tests.  Next, I want to ensure that CustomerManager does correct validation on the fields to ensure no bad data is entering our system.

[Test]
[ExpectedException(typeof(Exception))]
public void CreateNullCustomer()
{
    Customer c = CustomerManager.CreateCustomer(null, null);
    Assert.Fail(“Should not get here”);
}

That’s OK, but we are relying on SubSonic doing the validation, hence why we are expecting a System.Exception to be thrown.  Personally, I would prefer a ArgumentNullException to be thrown instead and I would also prefer CustomerManager to be doing the validation.

[Test]
[ExpectedArgumentNullException]
public void CreateNullCustomerWithOutValidation()
{
     Customer c = CustomerManager.CreateCustomer(null, null);
     Assert.Fail(“Should not get here”);
}

That feels better, notice how i’m using a cool MbUnit attribute to save a little bit of typing and make it more readable.

Next, I want to be able to return a Customer from the database based on their name. If the customer does not exist, I want an ArgumentException to be thrown.  I’m using MbUnit RowTest feature – Read more on this here.

[RowTest]
[Row(“John Smith”)]
[Row(“Unknown Customer”, ExpectedException = typeof(ArgumentException))]
public void GetCustomer(string name)
{
    CustomerManager.CreateCustomerWithoutValidation(“John Smith”, “”);
    Customer c = CustomerManager.GetCustomer(name);
    Assert.AreEqual(name, c.Name);
}

Now I can return one customer, I want to be able to return all the customers as a list.

[Test]
public void GetAllCustomers()
{
    string name = “John Smith”;
    CustomerManager.CreateCustomerWithoutValidation(name + ” A”, “”);
    CustomerManager.CreateCustomerWithoutValidation(name + ” B”, “”);
    CustomerManager.CreateCustomerWithoutValidation(name + ” C”, “”);
    List customers = CustomerManager.GetAllCustomers();
    Assert.AreEqual(3, customers.Count);
}

With the implementation looking like this:

public static List GetAllCustomers()
{
    SqlDataReader dr = (SqlDataReader)HowToUnitTestDB.Customer.FetchAll();

    List custs = new List();

    while (dr.Read())
    {
        Customer c = new Customer();
        c.Name = dr.GetString(1);
        c.Email = dr.GetString(2);
        custs.Add(c);
    }

    return custs;
}

While this test looks fine it does expose a problem with the tests as a whole and that is that we are not cleaning up after ourselves, like with the last post.  This is one of the problems with database testing as you need to ensure the tests don’t leave lasting data which can have knock on effects.

Executing the test, we receive this error message.

Error    1    TestCase ‘CustomerManagerTests.GetAllCustomers’ failed:  Equal assertion failed: [[3]]!=[[47]]
MbUnit.Core.Exceptions.NotEqualAssertionException
Message:  Equal assertion failed: [[3]]!=[[47]]
Source: MbUnit.Framework
StackTrace:
   at MbUnit.Framework.Assert.FailNotEquals(Object expected, Object actual, String format, Object[] args)
   at MbUnit.Framework.Assert.AreEqual(Int32 expected, Int32 actual, String message)
   at MbUnit.Framework.Assert.AreEqual(Int32 expected, Int32 actual)
   at SubSonic.Tests.CustomerManagerTests.GetAllCustomers() in E:UsersBen HallDocumentsVisual Studio 2008ProjectsHowToUnitTestSubSonic.TestsCustomerManagerTests.cs:line 69    E:UsersBen HallDocumentsVisual Studio 2008ProjectsHowToUnitTestSubSonic.TestsCustomerManagerTests.cs    69
   

To solve this, I will use the SetUp and TearDown fixtures, however I don’t really want a method to delete the customers on my CustomerManager ( CustomerManager.DeleteAllCustomers();) just to be able to clean up my test code.

[SetUp]
public void TestSetup()
{
    DeleteCustomers();
}

[TearDown]
public void TestTearDown()
{
    DeleteCustomers();
}

private static void DeleteCustomers()
{
    SqlConnection sqlConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings[“DatabaseConnection”].ConnectionString);
    SqlCommand sqlCmd = new SqlCommand(“TRUNCATE TABLE dbo.Customer”, sqlConn);

    try
    {
        sqlConn.Open();

        sqlCmd.ExecuteNonQuery();
    }
    finally
    {
        if (sqlConn.State == System.Data.ConnectionState.Open)
            sqlConn.Close();
    }
}

This is a bit naughty as we are accessing the database directly, however I feel it is the better of two evils in this situation, the more complex your data becomes maybe you would rethink, or if you already had a DeleteAllCustomers() method you could use that,  it’s just that in this case at the moment we don’t.

Hopefully by now your getting a good feel for unit testing with SubSonic, sadly we don’t mock anything but it doesn’t mean you can’t do it…  One final thing I want to talk about is verifying updates to a customer.

To update a customer, we need to know their ID.  At the moment, our public customer object doesn’t expose this information, however we can quickly modify this.

[Test]
public void GetCustomerID()
{
    string name = “John Smith”;
    CustomerManager.CreateCustomerWithoutValidation(name, “”);

    Customer c = CustomerManager.GetCustomer(name);

    Assert.AreEqual(1, c.ID);
}

In order for this to build, I need to add a ID property on the Customer object

public int ID { get; internal set; }

I can then modify my GetCustomer method to return the ID as well. All my previous tests ensure I haven’t broken anything along the way.  We can now write a correct test.

[Test]
public void UpdateCustomer()
{
    string name = “John Smith”;
    CustomerManager.CreateCustomerWithoutValidation(name, “”);
    Customer c = CustomerManager.GetCustomer(name);
    Assert.AreEqual(name, c.Name);

    c.Email = “JohnSmith@Gmail.com”;

    CustomerManager.UpdateCustomer(c.ID, c);
}

After that, the passing implementation looks like this:

public static void UpdateCustomer(int customerID, Customer c)
{
    HowToUnitTestDB.Customer dbCust = HowToUnitTestDB.Customer.FetchByID(customerID);
    dbCust.Name = c.Name;
    dbCust.Email = c.Email;
    dbCust.Save();
}

We can now insert, get and update customers.  Great! We just need to put a UI on it.  A simple console application will do:

class Program
{
        [STAThread]
        public static void Main(string[] args)
        {
            Console.WriteLine(“How To Unit Test: SubSonci”);
            string command = GetCommand();

            while (command != “Quit”)
            {
                switch (command)
                {
                    case “Insert”:
                        InsertCustomer();
                        break;
                    case “Get”:
                        GetCustomer();
                        break;
                    case “Update”:
                        UpdateCustomer();
                        break;
                    default:
                        break;
                }

                command = GetCommand();
            }
        }
}

…. The code is sightly too long to post here (download the solution at the end), but the InsertCustomer looks like this:

private static void InsertCustomer()
{
    Console.WriteLine(“Enter new customer details”);
    Console.WriteLine(“Name:”);
    string name = Console.ReadLine();
    Console.WriteLine(“Email:”);
    string email = Console.ReadLine();
    CustomerManager.CreateCustomer(name, email);
    Console.WriteLine(“Saved”);
}

Summary

In summary, you can unit test against subsonic, but it involves hitting with the database. I hope you have found this of some use and provided you with a good insight.  In a later post I will discuss how we can mock SubSonic (Hopefully). 

SubSonic can be found at http://www.codeplex.com/actionpack

Download Solution: HowToUnitTestSubSonic.zip

Technorati Tags: , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *