An Example of Test-Driven Development
GSoC: Implementing DataFrame in Pharo
In my previous post I have designed the public interface for the DataFrame project in Pharo. Now I will write some tests for making sure that the
DataFrame class supports (understands) that interface.
The Ideas Behind TDD
TDD is a radical process that promotes the notion of writing test cases that then dictate or drive the further development of a class or piece of code. This is often referred to as “writing tests first” (Craig Murphy).
It is known that the cost of making changes to the project tends to grow with time. The more changes we make the harder it becomes to introduce new changes. The downside of testing the project after all the vital parts were already implemented is that the bugs revealed by these test would take too much time to be fixed. It is even possible that the logic behind the system’s architecture was affected by these bugs, and fixing them would require us to redesign the whole project.
By “writing tests first” we create the failing tests (“red” or “yellow”) based on what we want to achieve and how we want our system to behave, and then, during the development phase we make them “green” one by one. Then we can add new tests, make them “green” and so on. This way bugs get spotted and eliminated on the earliest stage.
The steps described here are not some common practice. That is just something I do in my project. Something that can be seemingly good but horribly wrong.
Step 1: Initial Requirements
The initial project requirements were declared by me in my proposal. I am also planning to create some better documentation with a clear roadmap and project specifications.
I’ve started developing this project by designing the public interface and writing tests for it, before the actual behavior got implemented. This way I make sure that the interface does not reveal any information about the internal architecture of the project.
Step 2: Writing Tests for the Interface
The first test case called
DataFrameInterfaceTests contains tests that make sure that objects of the
DataFrame class understand the methods we want them to understand. These tests don’t check if the methods are implemented or if they do what they are supposed to. The only requirement is that these methods exist. This can be achieved by testing if a method call raises a
MessageNotUnderstood exception. For example, the following test will tell us if the object of
DataFrame understands the message
testNameIndexColumns self shouldnt: [
DataFrame new columns: #('Name' 'Allegiances').
] raise: MessageNotUnderstood.
At this point we don’t have any methods in our
DataFrame, that’s why all the interface tests fail (they are yellow, which means that the tested condition was not satisfied.
Step 3: Making them Green
Now let’s make these tests green by adding all the methods we want to implement to the
DataFrame without actually implementing them.
"I need to be implemented"
You can see that all the interface tests are now passing. Even though all these methods are empty, the
MessageNotUnderstood exception is not being raised, which means that the assertion is satisfied and the tests are green.
Now we have a
DataFrame class with an interface that was designed by us on a previous stage. If any of these methods will be renamed or removed, the tests have to be changed accordingly. This makes the potential changes of the interface more explicit.
Step 4: Behavior Testing and Implementation
At this stage the project can be uploaded to the master branch on GitHub (https://github.com/PolyMathOrg/DataFrame). Now we can fork multiple branches from master and provide different versions of a
DataFrame by implementing these methods. Each branch will have a set of test cases testing the behavior of a
DataFrame — whether or not the methods work as they are supposed to.