Before I start, I would like to point out
that if you are confused about differences between Mock and Stub objects, please read
the Fowler’s post on the subject:
http://www.martinfowler.com/articles/mocksArentStubs.html
I have seen too many “Unit tests” where developers
do not isolate the test to the unit (object/method) being tested. Yes
they are testing a business object, but to test its behavior they load half of the
object hierarchy in the project. Moreover
if your object gets initialized from external resource like a database, then they
create this complex “test databases” that contain their test data. These
databases have to be shared with other developers. If
you have a continuous build environment that runs tests as a part of the build process,
then you have to have a copy of the test database there. All
these test databases have to be modified as you modify schema/data in your development
database. In addition if your test modifies
data in the database then you need to setup an additional process in the SetUp or
Teardown to restore the data to initial state.
So the test should be simpler if we mock the
db dependency, right? So what does that
involve? Let’s say we have an object
called project Project with constructor as described below:
public Project(IProjectGateway gateway, int id)
{
using(IDataReader dr
= gateway.GetProjectBy(id)) {
if(dr.Read())
{
_id
= dr.GetInt32(0);
_name
= dr.GetString(1);
_date
= dr.GetDateTime(2);
}
}
}
Where IProjectGateway is an interface defining
a set of methods/and object in charge of persistence of the Project data to and from
the database. So
lets look how that interface might look like:
public interface IProjectGateway {
IDataReader GetProjectBy(int id);
...
}
So far this is simple, right?
Actual implementation of that interface does not matter for our test. Why? Because
we are not testing database, its resources/file storage, hardware, connection pooling,
network connections, etc. All we are
supposed to test is that our object properly populates its field from a returned DataReader.
To test the Project object without the database
roundtrip we will only need to mock the IProjectGateway, setting the expectations
for the GetProjectBy(id) to return our test data (data reader). I have to mention
that the dynamic mocks in the examples bellow were done using my favorite mocking
tool TypeMock.Net.
[Test]
public void AssureFetchMapsFields()
{
Mock<IProjectGateway>
projGatewayMock = MockManager.Mock<IProjectGateway>();
projGatewayMock.ExpectAndReturn("GetProjectBy", new ProjectDataStub().CreateDataReader());
Project project
= new Project(projGatewayMock.MockedInstance,1);
Assert.AreEqual(1,project.Id);
Assert.AreEqual("Test",project.Name);
Assert.AreEqual(DateTime.Parse("1/1/2000"),project.Date);
}
So lets see what exactly have we done in the
test above. First line creates our dynamic
mock instance. Second line is the interesting
part. It states that we expect one method
call on our IProjectGatewayMock, and that is “GetProjectBy()” method. Once
it is called we want the IDataReader to be returned from ProjectDataStub.CreateDataReader(). The
rest of the code instantiates the Project object and then assures that the Project’s
properties are initialized.
But what about this ProjectDataStub.CreateDataReader()? Well
I have noticed the ability of the DataTable objects to create an instance of TableDataReader(which
implements IDataReader). So theoretically
we could create a DataTable with column types that reflect the types of the actual
columns of the table/view we are fetching from db and populate this DataTable with
test record(s). Then our mock IProjectGateway
can return the IDataReader from this object, and voila – no db or any other external
connection used in the test.
So if we follow this logic the ProjectDataStub
should be an object that inherits from DataTable and populates its columns and rows
with test records when it is constructed. But
coding these stub object for each and every test might be a bit tedious. To
solve this problem I have created a rather simple tool that allows us to generate
this DataStub simply by copying and pasting select SQL statements from the fetch Stored
Procedure and executing it.
Above is the scren shot showing how tool works and its output. If you think
that this tool might be useful for you feel free to download the code from the link
bellow:
StubGenerator.zip
(780.62 KB)
Is this all when it comes to testing the DAL? No, obviously we have only tested
the data mapping part. Code in this Project constructor can throw SqlExcpetion
(database down, network problem, schema problem). We need to assure that our
code handles that.
Now putting the try/ctach block in the Project constructor does not make sense - I
do not want Project instantiated if we were unable to retreive its data. So
how do we test this case? Lets assume that our application is using Model View
Presenter architecture, and that the object instantiating our Project is the Presenter
object of the View that displays Project. Obviously we need to assure that this
Presenter can recover from the SqlException thrown by Project. Lets take a look
at the code bellow:
public class ProjectPresenter
{
....
public void DisplayProject(){
try{
Project
p = new Project();
...
do something with project like display it on the view etc...
}catch(SqlException
e){
Log.LoqException(e);
}
}
So if we can assure that the Log.LogException(e) is called when the Project throws
the SqlException, that would be a proof that the exception was handled.
Keep in mind that in Test Driven Development we would be writting the test prior to
the DisplayProject() method being written (and the catch block inside of it existing)
Dynamic mocking helps us here also. In this case we are testing the Presenter,
which means that the other two objects Log and Project would be mocked. Test
would encompass mocking ProjectConstructor instead of returning Project throws SqlException,
and then assuring that the Log Mock accepts the call to LogException(e).
In addition there would be tests that we need to run for Insert, Update and Delete
(if Project needs to support that), but I will leave that for a future post.
Be the first to rate this post
- Currently 0/5 Stars.
- 1
- 2
- 3
- 4
- 5