There’s been a lot of talk in the industry about using mocking frameworks for unit testing, and while I do think that these are useful I haven’t yet ventured down this highway.
What I’d like to blog about today is using stubs for unit testing. Stubs and mocks are not the same thing, as Martin Fowler points out in this post. A shortened version is that mocks verify behavior while stubs can verify both behavior and state.
I’m currently working on a project that extracts data from QuickBooks and converts it to an XML document that gets submitted to a host system.
The unit test I wrote needs to verify that I am creating the XML correctly (i.e. in the format that the host expects) for a given QuickBooks customer record. Here’s the unit test code:
[TestMethod()]
public void ConvertCustomerToXmlTest()
{
QbToXmlConverter target = new QbToXmlConverter();
String customerId = "12345";
String firstName = "Larry";
String lastName = "Parker";
String phone = "555-1212";
CustomerRetStub customer = new CustomerRetStub();
customer.CustomerID = new QBStringTypeStub(customerId);
customer.FirstName = new QBStringTypeStub(firstName);
customer.LastName = new QBStringTypeStub(lastName);
customer.Phone = new QBStringTypeStub(phone);
XElement customerXml = target.ConvertCustomerToXml(customer);
Assert.AreEqual("Customer", customerXml.Name);
Assert.AreEqual(customerId, customerXml.Attribute("CustomerNumber").Value);
Assert.AreEqual(lastName, customerXml.Attribute("LastName").Value);
Assert.AreEqual(firstName, customerXml.Attribute("FirstName").Value);
Assert.AreEqual(phone, customerXml.Attribute("Phone1").Value);
}
This test creates a new customer instance, submits it to the ConvertCustomerToXml method (which is the method being tested), and verifies that the XML returned by the method contains the expected structure and content.
And the test passes! 🙂
The signature of the ConvertCustomerToXml method looks like this:
XElement ConvertCustomerToXml(ICustomerRet customer)
Note that the customer parameter is of type ICustomerRet, which is a QuickBooks interface. This makes sense because we will ultimately get our data from QuickBooks, so we might as well pass the interface directly to our method.
But also note that the unit test above does not supply an ICustomerRet interface reference that came from a QuickBooks SDK call. Instead, it passes an instance of a class called CustomerRetStub. What is this?
The QuickBooks interfaces are COM-enabled interfaces, and the SDK only provides a single concrete class that can be instantiated, called QBPOSSessionManagerClass. This is used to open up a connection to the QuickBooks database, establish a session, create a message set request, and finally send a request and receive back a response.
That’s quite a bit of work for a unit test and is actually a bit far away from what we really want to test, which is to simply transform an ICustomerRet interface reference into XML that the host can receive.
Furthermore, QBPOSSessionManagerClass requires an actual QuickBooks database to connect to, which is a heavy dependency for a unit test (and technically starts moving us out of the realm of unit testing and into integration testing).
The way around all of this is to create our own class that implements ICustomerRet and use that to create a test customer, and then pass that into ConvertCustomerToXml. This is called a stub.
Here’s what the CustomerRetStub looks like:
public class CustomerRetStub : QBBaseStub, ICustomerRet
{
#region ICustomerRet members
...
public IQBStringType CustomerID { get; set; }
public IQBStringType CustomerType { get; set; }
public IDataExtRetList DataExtRetList { get; set; }
public IQBStringType DefaultShipAddress { get; set; }
public IQBStringType EMail { get; set; }
public IQBStringType FirstName { get; set; }
public IQBStringType FullName { get; set; }
...
#endregion
}
This essentially creates a bunch of properties that correspond to the ICustomerRet interface. The ICustomerRet interface itself just defines getter properties, but our concrete class needs to provide setter properties as well so we can set the values in our unit test.
Note also that the properties themselves are QuickBooks interfaces, and I had to stub these out as well.
Creating the stubs took some work, but it wasn’t too bad with the help of Visual Studio’s macro feature.
It’s important to point out that these stub classes are exactly that – stubs. They don’t provide the full functionality of the actual types being stubbed out, but instead just enough of what we need for our tests.
Now that we have the stubs, our test is easy. I’ll show it again, this time with the stubs highlighted in bold:
[TestMethod()]
public void ConvertCustomerToXmlTest()
{
QbToXmlConverter target = new QbToXmlConverter();
String customerId = "12345";
String firstName = "Larry";
String lastName = "Parker";
String phone = "555-1212";
CustomerRetStub customer = new CustomerRetStub();
customer.CustomerID = new QBStringTypeStub(customerId);
customer.FirstName = new QBStringTypeStub(firstName);
customer.LastName = new QBStringTypeStub(lastName);
customer.Phone = new QBStringTypeStub(phone);
XElement customerXml = target.ConvertCustomerToXml(customer);
Assert.AreEqual("Customer", customerXml.Name);
Assert.AreEqual(customerId, customerXml.Attribute("CustomerNumber").Value);
Assert.AreEqual(lastName, customerXml.Attribute("LastName").Value);
Assert.AreEqual(firstName, customerXml.Attribute("FirstName").Value);
Assert.AreEqual(phone, customerXml.Attribute("Phone1").Value);
}
A running QuickBooks database was not needed for our test, and we were able to test out exactly what we needed; i.e. that our XML was formulated the way we expected based on an ICustomerRet interface reference being passed to the ConvertCustomerToXml method.
As an aside, if we did want to create a test that uses the QuickBooks database and the actual QBPOSSessionManagerClass QuickBooks class, we could do something like this:
[TestMethod()]
public void ConvertCustomerIntegrationTest()
{
QbToCcConverter target = new QbToCcConverter();
IQBPOSSessionManager sessionMgr = null;
String qbConnStr = ConfigurationManager.AppSettings["MyConnectionString"];
try
{
sessionMgr = new QBPOSSessionManagerClass();
sessionMgr.OpenConnection("LPTest", "LPTest App");
sessionMgr.BeginSession(qbConnStr);
IMsgSetRequest request = sessionMgr.CreateMsgSetRequest(3, 0);
ICustomerQuery custQuery = request.AppendCustomerQueryRq();
IMsgSetResponse msgSetResponse = sessionMgr.DoRequests(request);
IResponse response = msgSetResponse.ResponseList.GetAt(0);
ICustomerRetList customers = response.Detail as ICustomerRetList;
ICustomerRet customer = customers.GetAt(0);
XElement customerXml = target.ConvertCustomerToXml(customer);
Assert.AreEqual("Customer", customerXml.Name);
Assert.IsNotNull(customerXml.Attribute("CustomerNumber").Value);
Assert.IsNotNull(customerXml.Attribute("LastName").Value);
Assert.IsNotNull(customerXml.Attribute("FirstName").Value);
}
finally
{
if (sessionMgr != null)
{
sessionMgr.EndSession();
sessionMgr.CloseConnection();
}
}
}
As you can see, there’s a bit more involved for this type of test. Also notice that the assertions just confirm that the XML contains something for the attribute values. Unless we know exactly what’s in the QuickBooks db that we’re using for our test, we can’t really write a test that checks for specific values in the XML.
But stubs allow us to bypass all of this and just focus on testing the specific method we’re interested in. And since we have control over the data we supply to the stubs in our test code, we can write better assertions against them.