A lot of junior coders often complained to me that it is very difficult to write a test case even though they acknowledged that test cases are very important.
As most coders are not trained to write test cases, it can be a very daunting task to do for the uninitiated. In this blog post, I will present a very simple approach to writing your test cases.
Before we go into writing a test case, you must understand what is test driven development (TDD). TDD means using test cases to drive your development. It can be broken down to three steps (in this specified order):
1) Red: Write a failing test case
2) Green: Write the code to pass the test case
3) Refactor: Refactor your code (including your test case code!)
We will write a test case first before writing the actual code. Naturally this test case will fail because there are no actual code to pass it (hence, red). Next we will write the actual code to pass the test (hence, green). Finally, we will refactor our actual code to make it better with the test case to protect us if we accidentally spoilt the code.
You can’t refactor your code confidently unless you have a test case to cover it.
So TDD can be summed up as Red-Green-Refactor. Through this cycle, we use test cases to drive our development. We achieve a higher code quality because we have a test case to cover our code which gives the affordance to refactor our code to be better.
Test cases are important because they:
So we know test cases are important. But why can’t we write them AFTER the code? Isn’t it better? Isn’t it easier?
There are several advantages to write your test cases FIRST (in the style of TDD):
A simple approach to write a test case is to think of behaviour, instead of implementation.
Let me give you an example.
Everyone knows how to use a calculator and how a calculator should behave.
We can use a calculator to help us to sum up numbers, say 1 + 1.
– Press 1
– Press +
– Press 1
– Press =
– Expect calculator to display 2
We understand easily what are the inputs we need and what are the outputs we expect.
When we perform the exact inputs, we expect the exact output.
If we don’t have the exact output, it means that the calculator is not working!
In the example above, do you know how does the calculator calculates 1 + 1 in order to return 2?
Unless you have knowledge of circuitry system, you will not be able to know the implementation.
However, without understanding the implementation of the calculator, we are still able to understand how should the calculator behave (ie. 1 + 1 should display 2).
This is the difference between behaviour and implementation.
So how do we translate thinking about behaviour into our test cases?
We just need to know the inputs and the outputs, and we can write our test cases.
In writing a test case, input is simply calling the method that we are testing and output is simply expecting the result from the method.
Input = Calling the method
Output = Expecting the correct result
So Input & Output becomes Call & Expect.
Let’s play with a very simple example: write a test case for the method sum() which will return the sum of two parameters. eg.
sum(1, 1) # => 2
Remember “Call & Expect”? To create our test case, we will call the method eg.
sum(1, 1) and we will expect the result to be 2.
With the above call and expect approach, the test case becomes:
RSpec.describe Calculator do describe '#sum' do it 'returns 2 for 1 and 1' do result = Calculator.sum(1, 1) expect(result).to eq 2 end end end
That’s it for our first test case! We can also easily triangulate it with another test case:
RSpec.describe Calculator do describe '#sum' do it 'returns 2 for 1 and 1' do result = Calculator.sum(1, 1) expect(result).to eq 2 end it 'returns 8 for 3 and 5' do result = Calculator.sum(3, 5) expect(result).to eq 8 end end end
This creates the test case first without us writing the code. Isn’t this easy? 🙂
Ok maybe sum() is a little too easy and not that practical. Let’s do something really tough.
In mathematics, the Leibniz formula for π, named after Gottfried Leibniz, states that
Based on the formula, the more fractions (eg. 1/3, 1/5, 1/7) we used, the more accurate the value of π we will get.
Write a method to calculate the value of π that accepts a number n which is the number of fractions used to approximate the value of π using Leibniz formula.
Take a moment to understand the problem but please don’t try to solve it.
Let me show you that is much easier to write the test case first, than the actual solution.
To write our test case, we think about ‘Call & Expect’ again.
In this problem, we will call the method with n which is an integer and we will expect it to give us the correct approximated π.
It is that easy.
Or not? er….
What should be the name of the method? Maybe
Where should I place this method? Maybe as a method under the module
How should I call this method? Simply as
What should be the result to expect? We can get this easily by simply calculating (in Rails console)
4 * (1 - (1.0/3) + (1.0/5) - (1.0/7) + (1.0/9) - (1.0/11) + (1.0/13)) which we will get
In conclusion, we will call
Calculator.approximate_pi(6) and we should expect
Call & Expect: We have derived our test case!
Here’s the code:
RSpec.describe Calculator do describe '#appromixate_pi' do it 'approximate pi to 3.283738 for 6 fractions' do result = Calculator.appromixate_pi(6) expect(result).to be_within(0.1).of(3.283738) end end end
As you can see from this example, it is much easier to create the test case than the actual solution itself.
From the approximate π example, while it is easy to think in terms of ‘call & expect’, it may not be that easy to derive what to call in the first place.
This is because we need to make the design decisions on how to name the method, where to place the method and how to call the method.
Don’t mistook these design decisions as additional work and time taken.
These design decisions have to be made even if you are writing the actual solution directly without the test case so there is actually no loss in time.
It is a matter of making the design decision in the test case before the actual code or in the actual code without the test case.
There is a positive side effect of making the design decision in the test case: You are using your object straight away in your test case just by the virtue of calling it. This allows you to validate if this is a good API to use. This side effect subconsciously drives better object design in your coding.
With the test case written, you can focus on writing the actual code now. I find that the test case generally helps me to focus on the code on hand instead of worrying about the entire structure/flow/interaction/things-not-done-yet/etc. This gives me great productivity!
Furthermore, because my mind does not worry about the other uncompleted tasks, I have less stress writing my code now.
Test case makes me happy. 🙂
After you have finished the actual solution and your test cases are passing, you can refactor immediately (instead of spending time now to write the test case).
This is great because you just finished the actual solution and this moment is the moment where you understand the code the most.
Instead of being distracted to write a test case, you can immediately work on refactoring your solution.
Furthermore, after finishing the actual solution, you have a better understanding of the problem/solution which helps you to refactor your test cases (either by creating other test cases or refining the test cases to test more accurately).
This really maximise productivity.
I hope I have convinced you why writing test cases first is important and also given you a simple easy approach to create test cases – Call & Expect.
Ultimately, we all want productivity and happiness for developers. Writing test case first gives you more productivity and happiness in your development. Go forth now!
Image source from https://en.wikipedia.org/wiki/Sensory_processing_disorder