Test Driven Design and levels of testing
Major benefit of TDD is not coverage,
regression stability, test problem solving - NO. It is - Test Driven Design
(Architecture). Test Driven Design is something that is being born of writing
tests before code, writing tests before functionality started to work,
writing the tests before you really clear about the rest of system (data
flow, integration etc.). Test Driven Design forces you to use old good patterns
as Factories, Template Methods.. it forces you to hide implementation behind
interface.. it forces you to separate concerns and divide functionality on small objects, each of it responsible for its small function
and could be tested exactly for its function. Instead of increase functionality by appending some code to already big function, you encapsulate new functionality to new object, test it in isolation and then easily integrate it to the place where it have to start work on.
The reason why Test Driven Design works is that doing a tests make you a class user, make you focus on its primary goal and interface and just make it impossible (or difficult and annoying) to make a class with multiple responsibilities.
How to get practical benefits of TDD, what level of testing should I consider to be sure I following the plan of TDD?
Disclaimer: all code samples are created very quickly, just to demonstrate ideas, should not be considered as working examples.
Level 1 - Unit tests
Here it all begins. Unit tests are kind of testing of really small parts of objects, like method properties and so on. This is there the actual requirements for the object is
created and verified. The major point of unit tests is - they have to be isolated! That means, all object(s), data, view etc. that tested object is
depend on, must be hidden behind corresponding interfaces. Please note, that at initial state those interface could not be defined at all (contains no methods) and its actual form will be dictated by actual object needs.
Object needs will be clarified on a way of applying next test cases to testable object.
You have to be always focus on business object or primary object. This is the one that you have to test first. If you are doing registration functionality on site, Registration
is class to start. If you are doing calculation application Calculate
is class to start. If you are doing web crawling application, Crawler
is class to start.
It is always like that - business object typically requires 2 things, data and view. In some particular case BO could depend on some external services, that would mean that interface to the service should pass to constructor as well. Also, in some cases it doesn’t require a view, just returns some data to outside consumer.
public class Some
{
public Some(IData data, IView view)
{
}
}
public interface IData
{
//empty
}
public interface IView
{
//empty
}
* This source code was highlighted with Source Code Highlighter.
Then tests are being created:
[TestFixture]
public class SomeTests
{
//create method
public Some CreateSome()
{
return Some(new DataMock(), new ViewMock());
}
[Test]
public SomeInScenarioOne() { }
[Test]
public SomeInScenarioTwo() { }
[Test]
public SomeInScenarioThree() { }
}
public class DataMock : IData
{
//some methods here
}
public class View : IView
{
//some methods here
}
* This source code was highlighted with Source Code Highlighter.
It is good design that constructor receives the interfaces only, the rest of parameters required supplied by corresponding methods arguments.
Once again, business object asks interfaces for required data or methods for displaying data. What exact data and methods are required, could be unknown at the beginning, it only depends on current business object needs. So, in a way of implementation goes interfaces of IData and IView started to form its shape. So, at the end they could look like this:
public interface IData
{
IQueryable<Columns> GetColumns();
void UpdateColumn(int id, string name, Column column);
void DeleteColumn(Column column);
}
public interface IView
{
void ShowSuccessMessage(string message);
void ShowFailMessage(string fail);
void DisplayResults(IQueryable<Column> columns);
}
* This source code was highlighted with Source Code Highlighter.
Note, that we are absolutely don’t care about actual implementation of both interfaces at the current moment. For instance Data
could use SQL database for columns, View
could call some Win32 API functions for displaying results. Doesn’t matter - forget about this at all, just continue with primary object and its functionality and tests.
Mock objects should provide required methods in most simple way, like:
public class DataMock : IData
{
public IQueryable<Column> GetColumns();
{
var list = new List<Column> { new Column(1), new Column(2) };
return list.AsQuearyable();
}
}
* This source code was highlighted with Source Code Highlighter.
For the different type of test data you could have a different mocks. Also, it is better to use some existing mock frameworks.
Level 2 - Data/View tests
As interfaces of Data/View granulated after level 1 testing, it is time to implement them. At this time we are really clear of what exactly we are going to implement, since there is already defined contract between client (primary object) and supplier (data/view classes).
Data/View implementation have to be tested also. There are more difficulties in testing them: data depends on SQL Server, that must be up and running, some test data have to be prepared before. View could not be tested in many cases. Netherless, why are implemented in same test before style and finally we have Impl
classes implementation. Ones that would be used with primary object after its integration to application.
Implementation
public class DataImpl : IData
{
private DataContext _context;
public DataImpl(DataContext context)
{
_context = context;
}
public IQueryable<Column> GetColumns();
{
return _context.Exec("SELECT * FROM COLUMNS").AsQueryable();
}
//rest of method implementation
}
public class ViewImpl : IView
{
public void ShowSuccessMessage(string message)
{
MessageBox(message);
}
//rest of method implementation
}
* This source code was highlighted with Source Code Highlighter.
Tests:
[TestFixture]
public class DataImplTest : IData
{
[Test]
public void GetColumns();
{
var data = new DataImpl(new DataContext());
var columns = data.GetColumns();
Assert.That(columns.Count,Is.EqualTo(10));
}
//rest of method implementation
}
* This source code was highlighted with Source Code Highlighter.
Level 3 - Integration tests
Integration tests are considered to be evil. I also think so, but I don’t think that it is possible to completely to avoid them. They are difficult to support and difficult to maintain them, but I think some number of integration tests have to be present to cover some typical scenarios of bugfix test cases.
In general, integration would mean - test the primary objects with concrete instances of dependent objects. Integration tests would cover a place where primary object is going to be integrated to. So, if we plan to use Some
in SomeAnother
you have to test the impact of this class after integration.
[TestFixture]
public class SomeIntegrationTests
{
[Test]
public void IntegrationScenario()
{
//arrange
var some = new Some(new DataImpl(), new ViewImpl());
//act
var results = some.Act(param1, param2);
//post
//do assert
}
[Test]
public void IntegrationScenarioTwo()
{
//arrange (some is integrated to SomeAnother)
var someAnother = new SomeAnother(new DataImpl(), new ViewImpl());
//act
someAnother.Act();
//post
//assert on actual influence of Some class on SomeAnother class
}
}
* This source code was highlighted with Source Code Highlighter.
Level 4 - Spec tests
The final level is Spec tests. BDD (Behavior Driven Development) is next generation of TDD, focusing on user acceptance criteria’s as development driver. There are quite good tools for that already like, SpecFlow or StoryQ.
Having those type of testing on Level 4, doesn’t mean that they are created after functionality is ready. It is recommended to create such tests first working with product owner and defining a test stories.
Personally I have no rich experience in BDD, but ideas and success stories with Cucumber (Ruby) makes them worth to look at.
Conclusion
Testing is important. But testing to create tests are pointless. Tests have to be created to create good design, easy to read and maintain code.
By doing tests right, they won’t complicate your life but make it easier.
Major benefit of TDD is not coverage, regression stability, test problem solving - NO. It is - Test Driven Design (Architecture). Test Driven Design is something that is being born of writing tests before code, writing tests before functionality started to work, writing the tests before you really clear about the rest of system (data flow, integration etc.). Test Driven Design forces you to use old good patterns as Factories, Template Methods.. it forces you to hide implementation behind interface.. it forces you to separate concerns and divide functionality on small objects, each of it responsible for its small function and could be tested exactly for its function. Instead of increase functionality by appending some code to already big function, you encapsulate new functionality to new object, test it in isolation and then easily integrate it to the place where it have to start work on.
The reason why Test Driven Design works is that doing a tests make you a class user, make you focus on its primary goal and interface and just make it impossible (or difficult and annoying) to make a class with multiple responsibilities.
How to get practical benefits of TDD, what level of testing should I consider to be sure I following the plan of TDD?
Disclaimer: all code samples are created very quickly, just to demonstrate ideas, should not be considered as working examples.
Level 1 - Unit tests
Here it all begins. Unit tests are kind of testing of really small parts of objects, like method properties and so on. This is there the actual requirements for the object is created and verified. The major point of unit tests is - they have to be isolated! That means, all object(s), data, view etc. that tested object is depend on, must be hidden behind corresponding interfaces. Please note, that at initial state those interface could not be defined at all (contains no methods) and its actual form will be dictated by actual object needs. Object needs will be clarified on a way of applying next test cases to testable object.
You have to be always focus on business object or primary object. This is the one that you have to test first. If you are doing registration functionality on site, Registration
is class to start. If you are doing calculation application Calculate
is class to start. If you are doing web crawling application, Crawler
is class to start.
It is always like that - business object typically requires 2 things, data and view. In some particular case BO could depend on some external services, that would mean that interface to the service should pass to constructor as well. Also, in some cases it doesn’t require a view, just returns some data to outside consumer.
public class Some
{
public Some(IData data, IView view)
{
}
}
public interface IData
{
//empty
}
public interface IView
{
//empty
}
* This source code was highlighted with Source Code Highlighter.
Then tests are being created:
[TestFixture]
public class SomeTests
{
//create method
public Some CreateSome()
{
return Some(new DataMock(), new ViewMock());
}
[Test]
public SomeInScenarioOne() { }
[Test]
public SomeInScenarioTwo() { }
[Test]
public SomeInScenarioThree() { }
}
public class DataMock : IData
{
//some methods here
}
public class View : IView
{
//some methods here
}
* This source code was highlighted with Source Code Highlighter.
It is good design that constructor receives the interfaces only, the rest of parameters required supplied by corresponding methods arguments.
Once again, business object asks interfaces for required data or methods for displaying data. What exact data and methods are required, could be unknown at the beginning, it only depends on current business object needs. So, in a way of implementation goes interfaces of IData and IView started to form its shape. So, at the end they could look like this:
public interface IData
{
IQueryable<Columns> GetColumns();
void UpdateColumn(int id, string name, Column column);
void DeleteColumn(Column column);
}
public interface IView
{
void ShowSuccessMessage(string message);
void ShowFailMessage(string fail);
void DisplayResults(IQueryable<Column> columns);
}
* This source code was highlighted with Source Code Highlighter.
Note, that we are absolutely don’t care about actual implementation of both interfaces at the current moment. For instance Data
could use SQL database for columns, View
could call some Win32 API functions for displaying results. Doesn’t matter - forget about this at all, just continue with primary object and its functionality and tests.
Mock objects should provide required methods in most simple way, like:
public class DataMock : IData
{
public IQueryable<Column> GetColumns();
{
var list = new List<Column> { new Column(1), new Column(2) };
return list.AsQuearyable();
}
}
* This source code was highlighted with Source Code Highlighter.
For the different type of test data you could have a different mocks. Also, it is better to use some existing mock frameworks.
Level 2 - Data/View tests
As interfaces of Data/View granulated after level 1 testing, it is time to implement them. At this time we are really clear of what exactly we are going to implement, since there is already defined contract between client (primary object) and supplier (data/view classes).
Data/View implementation have to be tested also. There are more difficulties in testing them: data depends on SQL Server, that must be up and running, some test data have to be prepared before. View could not be tested in many cases. Netherless, why are implemented in same test before style and finally we have Impl
classes implementation. Ones that would be used with primary object after its integration to application.
Implementation
public class DataImpl : IData
{
private DataContext _context;
public DataImpl(DataContext context)
{
_context = context;
}
public IQueryable<Column> GetColumns();
{
return _context.Exec("SELECT * FROM COLUMNS").AsQueryable();
}
//rest of method implementation
}
public class ViewImpl : IView
{
public void ShowSuccessMessage(string message)
{
MessageBox(message);
}
//rest of method implementation
}
* This source code was highlighted with Source Code Highlighter.
Tests:
[TestFixture]
public class DataImplTest : IData
{
[Test]
public void GetColumns();
{
var data = new DataImpl(new DataContext());
var columns = data.GetColumns();
Assert.That(columns.Count,Is.EqualTo(10));
}
//rest of method implementation
}
* This source code was highlighted with Source Code Highlighter.
Level 3 - Integration tests
Integration tests are considered to be evil. I also think so, but I don’t think that it is possible to completely to avoid them. They are difficult to support and difficult to maintain them, but I think some number of integration tests have to be present to cover some typical scenarios of bugfix test cases.
In general, integration would mean - test the primary objects with concrete instances of dependent objects. Integration tests would cover a place where primary object is going to be integrated to. So, if we plan to use Some
in SomeAnother
you have to test the impact of this class after integration.
[TestFixture]
public class SomeIntegrationTests
{
[Test]
public void IntegrationScenario()
{
//arrange
var some = new Some(new DataImpl(), new ViewImpl());
//act
var results = some.Act(param1, param2);
//post
//do assert
}
[Test]
public void IntegrationScenarioTwo()
{
//arrange (some is integrated to SomeAnother)
var someAnother = new SomeAnother(new DataImpl(), new ViewImpl());
//act
someAnother.Act();
//post
//assert on actual influence of Some class on SomeAnother class
}
}
* This source code was highlighted with Source Code Highlighter.
Level 4 - Spec tests
The final level is Spec tests. BDD (Behavior Driven Development) is next generation of TDD, focusing on user acceptance criteria’s as development driver. There are quite good tools for that already like, SpecFlow or StoryQ.
Having those type of testing on Level 4, doesn’t mean that they are created after functionality is ready. It is recommended to create such tests first working with product owner and defining a test stories.
Personally I have no rich experience in BDD, but ideas and success stories with Cucumber (Ruby) makes them worth to look at.
Conclusion
Testing is important. But testing to create tests are pointless. Tests have to be created to create good design, easy to read and maintain code.
By doing tests right, they won’t complicate your life but make it easier.