When unit-testing classes which have a complex dependency matrix we use faking a lot. We create fake objects, mock or stubs, we define their behavior in respect to the needs of the test. We need to do that for every test. Sure when a common pattern emerges, we create helper methods in order to remove duplicate code. Sometimes we need to have a lot of these helper methods, sometimes we need none and are happy to create are dependencies manually inside each test method.
When unit testing objects those outside dependencies are not really important. How they behave is not important. What is important is how our logic of the class being tested behaves based on the conditions we set for it.
The main proposition of the "Define once, recreate every time" unit-test pattern is that when defining each dependency there is some common code which needs to be written only once, and there is also a common behavioral state which also needs to be defined only once.
In this pattern all dependencies and their return values are defined as common members of the test fixture, e.g. testing class. Those dependencies are created each time a new before a test is being run in the SetUp method of the test fixture. This will ensure that each test, no matter the order its run, will have a clean default state upon which to make its assertions.
If a tests needs to make changes to result values or how a mock will behave with a given set of parameters it can modify those dependencies freely without worrying of breaking some future tests since the default state will be reset each time before each test is run.
In this example we have the following classes and interfaces:
- Database
- IAccountDataService
- AccountDataService
- IAccountRepository
- AccountRepository
- IAccount
- Account
- AccountRepositoryTest
The following code example will use these technologies:
Database. This is the relational, key value, or some other data store where our application permanently stores its data. We really do not want to include it in our tests. It will slow them down, we need to insert and clean up dummy data. It is a lot of work for which we may not have enough resources.
In our case in the database we have the following table:
CREATE TABLE Account
(
Id INT PRIMARY KEY NOT NULL,
Name Varchar(256) NOT NULL,
Amount Decimal(10,2) NULL
)
The Account table is used for storing data about our IAccount entity which is going to be used all during this example.
IAccountDataService and AccountDataService. The IAccountDataService is the interface we call upon in the rest of the application to store and read data about an IAccount entity from the permanent data storage. The AccountDataService is the concrete realization of the interface that actually gets to go to the database and retrieve the data requested by someone else. We use and interface here in conjunction with an Inversion of Control container in order to be able to mock the AccountDataService class in unit test for those classes of which this class is a dependency since we really do not want to test the AccountDataService class nor do we want to hit the database.
The IAccountDataService would be placed in a separate library, whose only task would be the direct communication with the database. The library will make use of primary types as parameters (integers, string , booleans etc. ) and DataTable as result types in order to abstract the data access layers from the domain library of the application. This will give us several benefits. For one we can safely replace the permanent data source for something else, another database or web service and our contract would not change since would still returns DataTable and receive primary types as values. It also leaves the data access layer blissfully ignorant about our domain layer and the business logic it implements. Our data access layer does one thing and one thing only it reads and saves data as requested without thinking it at all.
The AccountDataService class has the following methods:
public DataTable Get(int accountId)
{
//.. sql retrieval code here
DataTable result = new DataTable();
result.Columns.Add("Id",typeof(int));
result.Columns.Add("Name",typeof(string));
result.Columns.Add("Amount",typeof(decimal));
//.. get the DataReader
while(reader.Read()) //This will execute only once
{
DataRow row = result.NewRow();
row["Id"]=reader["Id"];
row["Name"]=reader["Name"];
row["Amount"]=reader["Amount"];
result.Rows.Add(row);
}
return result;
}
The Get(int):DataTable methods has the task of going to the database and retrieving the data needed to fill a specific entity based on its id.
IAccount and Account. IAccount is our interface for out business entity Account. It may or may not have some business logic in it. As our system will grow it will surely start to have some logic so it is good to prepare it for being mocked in tests.
public DataTable Save(int id, decimal amount, string name)
{
//.. insert or update logic here
DataTable result = new DataTable();
result.Columns.Add("Id",typeof(int));
result.Columns.Add("Name",typeof(string));
result.Columns.Add("Amount",typeof(decimal));
DataRow row = result.NewRow();
//in case of a insert the intial value is 0,
//but now is the LAST_INSERT_ID of the database
row["Id"] = id ;
row["Name"] = name;
row["Amount"] = amount;
result.Rows.Add(row);
return result;
}
The Save(int, float, string):DataTable method has the task of inserting or updating a single Account record in the database. The save method will be determined by the id parameter of the method. If its values is zero it will be an insert operation, if its something else it will be and update operation. In case of an insert the method will retrieve also the new id of the record and return it in the DataTable containing the newly updated record.
IAccountRepository and AccountRepository. This is our domain repository class for the IAccount entity. It handles all logic related to the reading and saving of a IAccount entity. This is also our class being tested. It has two dependencies. One is the IAccountDataService from which it gets the raw data it needs to make IAccount objects, and the other is the IAccount entity which is passed as argument to it or is returned by it. Both dependencies come to the AccountRepository object through the Inversion of Control container.
The IAccountRepository is located in the same layer as the IAccount entity. Given its dependency to the IAccoundDataService object it doesn't have to worry about the specifics of how the IAccount entity is saved or retrieved. Its only goal is to worry about the business logic of saving and retrieving the IAccount entity. In case of a future development where the IAccount entity will store and retrieve a part of its data structure from somewhere else, e.g. a file saved to the disk it will be the job of the IAccountRepository to coordinate those sets of operations also.
Another good thing of this structure is that the IAccountRepository object is the only interaction point between the data layer of the application and the domain layer. Which means that the entire domain layer is oblivious how the IAccount object is stored. This will enable us during integration testing to switch the permanent storage to another one, if possibly, memory based like Sqlite or just plain objects.
The AccountRepository class looks like this:
public class AccountRepository:IAccountRepository
{
private IAccountDataService accountDataService = null;
public AccountRepository()
{
accountDataService = ObjectFactory.GetInstance();
}
private IAccount mapDataRow(DataRow row)
{
IAccount account = ObjectFactory.Get();
account.Id = row.Field("Id");
account.Name = row.Field("Name");
account.Amount = row.Field("Amount");
return account;
}
}
The IAccountDataService is a private field of the class. It makes senses since it is used by the most of the methods contained in the AccountRepository class. The specific instance of the IAccountDataService used is instantiated in the constructor of the class using the StructureMap inversion of control container. We are working here with interfaces, so during testing we can and will replace the default instance of the IAccountDataService with a fake one for our testing purposes.
The mapDataRow(Row):IAccount method is used through the class to map a DataRow returned by the IAccountDataService to an IAccount instance.
The AccountRepository class has the following methods:
public void Get(int accoundId)
{
DataTable table= accountDataService.Get(accountId);
if (table.Rows.Count == 0 ) return null;
DataRow row = table.Rows[0];
IAccount account = mapDataRow(row);
return account;
}
The Get(int):IAccount method is very simple. It calls to the IAccountDataService instance with an integer parameter and transforms the returned data into a IAccount entity if possible. Since we are requesting a entity with a specific id the IAccoundDataService will not return more then one row. In case the IAccount row wasn't found with the given id the Get method will return a null value for higher layers to handle as appropriate.
public void Save(IAccount account)
{
if (account == null)
{
throw new ArgumentNullException("IAccount cannot be null.");
}
var table = accountDataTable.Save(account.Id,
account.Amount,
account.Name);
if (table.Rows.Count==0)
{
throw new ApplicationException("No data returned by the data service.");
}
DataRow row = table.Rows[0];
account = mapDataRow(row);
}
The Save(IAccount):void method saves an IAccount instance and then returns a completely filled IAccount instance by reference, e.g. in case of a new IAccount instance the Id property has a value of 0 and is replaced after the Save operation by the newly assigned Id property.
The Save method makes a few checks to make sure everything is all right. First it checks if the IAccount entity is null, in which case it breaks the method execution by throwing an ArgumentNullException. The second check is related to the result returned by the IAccountDataService Save method. If the result returned zero row it throws yet another exception relating to a possible data service error.
IAccount and Account. In this example the IAccount entity servers primarily as a data holder holding data to and from the permanent storage mechanism. As the business domain grows it will also grow with the addition of new fields and domain logic methods. In order to handle this future complexity we used the IAccount interface to facilitate its mocking. Also, to be in sync with the letter of the pattern we are going to mock it also, as it is , besides it being only a collection of three properties. A dependency is a dependency, and the smallest of mistakes can throws us on a wild goose chase in a wrong class searching for bugs that originated somewhere else.
AccountRepositoryTest is our unit-testing fixture for the AccountRepositoryClass. Its basic structure is this:
[TestFixture]
public class AccountRepositoryTest
{
...
}
Before we start writing test we need to identify what our dependencies are and what result they will generally provide for us.
The AccountRepository class has two dependencies as specified above. The first is the IAccountDataService interface and the second is the IAccount entity. We should not worry about it right now. A simple mocked object with automatic property handling will do. The IAccountDataService instance in its both methods it returns a DataTable object, this is our result. Both dependencies and the DataTable result need to be defined in advance.
[TestFixture]
public class AccountRepositoryTest
{
private MockaccountDataServiceMock = null;
private DataTable dataTable = null;
private DataRow firstRow = null;
private MockaccountMock = null;
}
We have create a mock object for the IAccountDataService and its result a DataTable and a DataRow object. We have made the DataRow object firstRow available as a private field of the test class since there exits a possibility that in some test we may need to change the data returned by the data row, and calling a direct reference is the easiest way of doing it.
Each dependency and its results need to be instantiated to a clean, default state before each test is run thus preventing one test to influence another by modifying the shared dependencies and results. This is done by using the SetUp method of the unit-testing method. This method is execute every time before a test is run.
[SetUp]
public void SetUp()
{
dataTable = new DataTable();
dataTable.Columns.Add("Id",typeof(int));
dataTable.Columns.Add("Name",typeof(string));
dataTable.Columns.Add("Amount",typeof(decimal));
firstRow = dataTable.NewRow();
firstRow["Id"] = 1;
firstRow["Name"] = "Marko Horvat";
firstRow["Amount"] = 44.00;
accountDataServiceMock = new Mock();
accountMock = new Mock();
accountMock.SetupAllProperties();
accountMock.Object.Id = 1;
accountMock.Object.Name = "Marko Horvat";
accountMock.Object.Amount= 44.00;
ObjectFactory.Initialize(x => {
x.For().Use(accountDataServiceMock.Object);
x.For().Use(accountMock.Object);
});
}
Now, we are sure that on every test run we are going to have a clean slate in respect to our dependencies and result values. It is important to note the last code block in the SetUp method. This is a StructureMap initialization pattern where we tell the IoC container which instances to bind when a client piece of code requests a specific type, e.g IAccountDataService. This piece of functionality enables us to embed our fake dependencies in our real class without changing its structure and behavior.
Lets us now write some tests and see how this works in real life:
public void Save_ProperParameters_AccountSaved()
{
accountDataServiceMock.Setup( x => x.Save(It.IsAny (),
It.IsAny(),
It.IsAny()))
.Returns(dataTable);
var instance = new AccountRepository();
var result = instance.Save(accountMock.Object);
Assert.IsNotNull(result);
Assert.AreEqual(result.Id, accountMock.Object.Id); // 1
}
This is a test written using the "Define once, recreate every time" pattern. On the start of the test we define the expected behavior of our dependency fake. We tell it that regardless of the input parameters to return the dataTable result we have defined previously. Then we create an instance of the AccountRepository class and with it we executed its Save method using the accountMock member as a parameter.
Now lets write another test, this time we will check the behavior of the Save method in case there are errors:
[Test]
public void Save_ErrorRaised_ThrowsExceptions()
{
accountDataServiceMock.Setup(x => x.Save(It.IsAny (),
It.IsAny(),
It.IsAny()))
.Returns(new DataTable());
var instance = new AccountRepository();
Assert.Throws( () => instance.Save(null));
Assert.Throws( () => instance.Save(accountMock.Object));
}
In this test I needed a different behavior from the Save method. I knew it was a new clean instance, for which no behavior was defined so I freely defined a new behavior for the fake. The following two assertions were simple enough. First I sent a null as the IAccount parameter in order to check if the Save method correctly threw theArgumentNullException, and then I checked that if the Save method of the IAccountDataSource returned an empty data source if the repository code correctly threw the ApplicationException. This was possible courtesy of the accountDataSourceMock object.
The "Define once, recreate every time" unit testing pattern offers the following benefits:
- Simplifies unit test writing
- Increases test writing speed
- Reduces the chances of one test modifying the results of another test
Nice overview of repository test techniques!
ReplyDeleteBut I would rather not see DataTable object anywhere inside my project :) Using Unit of Work pattern in Repositories database access can be effectively abstracted and then repository behavior tested without db dependencies. At least that's how I do things...
Sure. I generally prefer when I have the chance to use the Flent NHibernate automapping property to generate a mock database in memory using the SQlLite provider. Or to discontinue database testing at all in my unit tests.
ReplyDelete