Introduction to unit testing with SnapDAL

SnapDAL is deeply integrated with another Sourceforge.net project called .Net Mock Objects. This project, among other useful things it has to offer, provides Mock Object implementations for the generic data access classes of ADO.net. These mock objects include implementations for IDataCommand, IDataParameter, IDbConnection, IDataReader and IDataAdapter. So what good does that do me? Normally when you write code that accesses external data, that code is tied to the datasource in a way that requires it to be there when you run your tests. On small projects it's not a big deal. You have a development copy of the database, and a live copy. You write your tests, your code is configured to look for data in the test database, and you're good to go. As your project ages though, you start to have problems.

Even more challenging is the fact that even with a test database, your code quite often is configured in a way that makes it difficult to change the actual source of the data. Each class may have a reference to a data layer in the N-tier style of development, or to a manager if the Object Relational style of development that makes it very difficult to say something like, "I want all the db calls in this test to go to my test database, but I want my test to provide the data for class foo while testing".  Mock objects help answer the question of how to accomplish this and you may wish to review our basic introduction to standard .net Mock code to see how this would be accomplished without SnapDAL.

That's great, but of course it's not so simple. It's much more common to write classes in styles where not even the specific connection is left to the discretion of the caller, much less handing a connection to a method call. You would be very unlikely to do that as the connection is not related to the call at all. This is where SnapDAL comes in. In the introduction and the samples, you should have seen that it is possible with configuration only to change the provider and connection string. In this way a class can simply pull configuration information out of appsettings or a similar mechanism, and execute away. So how do you then substitute mock objects? SnapDAL has a Mock provider type built in. So for the simpler case above, you would simply load your DataFactory with a provider type of "Mock", and then each connection, command, reader etc, would be a mock version instead of the real version. SnapDAL fully supports the setting of expections on the supplied mock objects. However, consider this variation. Both the connection and provider are parameters derived from configuration and the object is managed by a repository.

class FooWithHiddenDataAccess
{
	string _conn = ConfigurationSettings.AppSettings["ConnectionString"];
	string _provider = ConfigurationSettings.AppSettings["Provider"];
	DirectoryInfo _statementsDir = new DirectoryInfo("../samplestatements");
	DataFactory _dal;
	public string Company;
	public string ContactTitle;

	public FooWithHiddenDataAccess() {
		_dal = new DataFactory(_conn, _provider, true, _statementsDir);
	}
		
	public void Load(string customerid)
	{
		HybridDictionary parms = new HybridDictionary(1);
		parms.Add("customerid", customerid);
		IDataReader rdr = _dal.ExecuteDataReader("query_customer", parms);
		using (rdr)
			if (rdr.Read())
			{
				Company = rdr["CompanyName"].ToString();
				ContactTitle = rdr["ContactTitle"].ToString();
			}
	}
}

What's important here is that the class is responsible for creating it's data access, making it a "Black Box" as far as the test goes. The test doesn't get to set this up except to vary configuration parameters. If you are thinking ahead, you realize you can set the provider to "Mock" and then the class will create mock objects instead of real objects. But does that help you? Mock objects won't automatically return readers so how does that work? Since the code under test is creating them, your test classes don't have a reference to them, so you can't execute any tests, right? Not right.

SnapDAL is built on the idea of caching statements for you. Since the statements are cached, and it is safe to assume that the test will run in the same app domain as the code under test in almost all cases, the test can pull the statements out of the cache to get a direct reference to same mock objects the class will use. Most importantly, SnapDAL provides a way for your test to inject a MockDataReader into the command object the code under test will use, plus adds some convienient ways to build these from test data in files, or in another database specifically setup for your test. Even if the class injects it's own sql statements without loading statements from files, as long as the class uses named commands, you can provide readers, and get references to the mock commands used by the class.

Here is a test for this class using standard mock techniques of setting expectations

[Test]
public void StandardMockStyle()
{
	//setup the mock command in SnapDAL
	DataFactory fac = new DataFactory(conn, "Mock", true, new DirectoryInfo(samplestatements));
	MockCommand comm = (MockCommand) fac.GetCommand("query_customer");
	MockDataParameter mParm = (MockDataParameter) comm.Parameters["@customerid"];
	mParm.SetExpectedValue("ALFKI");


	//create the mock reader in code
	DataTable schema = new DataTable();
	schema.Columns.Add("CompanyName", typeof(string));
	schema.Columns.Add("ContactTitle", typeof(string));
	MockDataReader mrdr = new MockDataReader();
	object[,] rows = new object[1,2];
	rows[0,0] = "test";
	rows[0,1]= "foo for test";
	mrdr.SetRows(rows);
	mrdr.SetSchemaTable(schema);
	mrdr.SetExpectedReadCalls(1); //not done in a loop so there should only be one read call
	mrdr.SetExpectedNextResultCalls(0);
	mrdr.SetExpectedCloseCalls(1);

	comm.SetExpectedReader(mrdr);
	comm.SetExpectedExecuteCalls(1);
	comm.SetExpectedCommandType(CommandType.Text);

	//here we grab the IDbCommand that the Foo class will use and substitute our Mock command
	StatementManager sqlCache = StatementManager.Create(true, new DirectoryInfo(samplestatements), "SqlClient");
	Statement sqlStatement = sqlCache.GetStatement("query_customer");
	sqlStatement.Command = comm;

	//execute and behold the magic!
	FooWithHiddenDataAccess foo = new FooWithHiddenDataAccess();
	foo.Load("ALFKI");

	Assertion.AssertEquals("didn't load company data with mock reader", "test", foo.Company);
	Assertion.AssertEquals("didn't load product data with mock client", "foo for test", foo.ContactTitle);

	//here the test can verify that the correct parameters, read calls and connections were passed
	//to the underlying objects
	comm.Verify();
	mrdr.Verify();
	mParm.Verify();

}

This test is a white box scenario in that the test does need to know what query name the code under test is using. Not the sql, just the name of the query, which is "query_customer", a simple parameterized query on the Northwind database. Here is a breakdown of the steps.