If you want to practice TDD there are two main approaches to choose from:
mockist TDD or classic TDD (Martin Fowler). In this post i would like to compare between the two.
First, I’ll describe the two approaches and later I will list the pros and
cons of the mockist approach.
Mockist TDD
With this approach, you're working in a high granularity, meaning every
class has its own test fixture. As a result, each test fixture should only
test one CUT (Class Under Test) and none of
the classes the CUT depends on, assuming they all have their own test fixtures.
Suppose we have a class A that uses class B. To achieve the high granularity we’ve talked about, TestFixtureA must use a Test Double of B, as shown in figure 1:
Figure 1 |
Of course our design must support dependency injection to achieve that
and it means class A must work against an interface of B and it also requires a
way to inject a concrete instance of B into class A (via constructor/setters/IoC
etc.)
That’s why this approach is called Mockist TDD since it has an
extensive use of Mocks (Test Doubles). It is also called Isolated Testing
since each class is tested in an isolated way.
NOTE: we isolate class A from class B even if class B is a regular business class that has no interactions with any external resources such as DB\web service\files system etc.
Classic TDD
With this approach, you're working in a low granularity, meaning every graph
of classes has its own test fixture. As a result, each test fixture covers a
graph of classes implicitly by testing the graph's root.
Figure 2 |
Usually you don't test the inner classes of the graph explicitly since they are already tested implicitly by the tests of their root, thus, you avoid coverage duplications. This lets you keep the inner classes with an internal access modifier unless they are in use by other projects.
Pros & Cons
Let’s describe the pros and cons of the mockist approach.
Pros
1.
More TDD’ish –
since all the classes the CUT depends on are mocked, you can start testing the
CUT without implementing the classes it depends on. Think about the classic
approach, when you come to test some CUT, you should first implement its
dependencies, but before that you should first implement their dependencies and
so forth.
2.
High granularity –
this means:
a.
Smaller test fixtures – one
per class, unlike one per graph of classes in the classic approach.
b.
Smaller test setups – take
a look on TestFixtureA at figure 2: the Arrange phase
(Arrange-Act-Assert) of tests like this is quite large since it has to setup a state
for too many classes in the graph. This quite a crucial issue – the
bigger the test, the bigger the risk of having bugs in the test itself.
c.
Frequent checkins/commits –
think about it, with the classic approach, your tests won’t pass before all the
classes the CUT depends on are implemented correctly, thus, the frequency of
your checkins (commits) is reduced dramatically (you don't want to commit red tests).
3.
More alternatives to
do DI – take a look at figure 3, suppose you need to inject different
concretes of interface I into class C. With the mockist approach, which heavily
relies on injections, the code that initializes class A also initializes class
B and inject it to class A, and also initializes class C and inject it to class
B. Therefore, it can easily inject a concrete class of interface I into class C. See figure
4 for example. On the other hand, with the classic approach, the code that
initialize class A doesn’t have access to the inner classes of the graph (B, C and I) and
therefore its only way to inject a concrete class of interface I into class C is by using some framework of IoC.
1.
Much more interfaces
and injections – with the mockist approach, for almost every class, you have
at least one interface. In addition, there is some kind of injections
inflation.
2.
Weaker encapsulation
– each class exposes its relations with the classes it depends on so that
they can be injected into it and also to allow behavior verification,
this partly weakens the
encapsulation.
3.
High vulnerability
to refactoring – with the mockist approach, every change in the interaction
between two classes, requires changes in some tests, since tests usually aware of
the interactions between classes (see behavior verification). With the
classic approach, on the other hand, you usually do state verification and
thus, the tests are not aware of the interaction between classes.
Conclusions:
I personally definitely prefer the mockist approach for one main reason –
I cannot see how truly TDD is possible without it.
Thank you for the great post. It describes very well the two approaches. Personally I agree with your conclusion and I also prefer the mockist approach. Having said that, I also test several classes as a component as well.
ReplyDeleteHi Boris, thanks for you comment :)
ReplyDeleteYou absolutely right, there is a place for a pragmatic approach - there will be cases in which some classes will not be mocked. For example classes that are already exist at the time you build you CUT (class under test) and you can use them immediately, or classes that are quite small and it doesn't take much to build them nor to initiate them.