Practicing TDD (Test Driven Development)
in STS 4.0.1 (Spring Boot) Test Driven Development
Test Driven Development (TDD) was a book I read.
Also from last year... and I'm only now posting about it. For now I'll post only the Java part. It's shorter than I thought, so it should be fine to write alongside other posts.
The book tells a fictional story: a company owns a program called WhyCash and a client asks for some feature changes (adding multi-currency support). The project is carried out by applying TDD and iteratively improving the code.
Alright, let's begin. My TDD test environment is as follows.
- OS : mac
- STS : 4.0.1
- Build : Gradle
- Framework : JUnit
1. ~The author makes a subtle joke: If you're a genius, you don't need these rules. If you're a fool, these rules won't help you either. But the majority of people in between can fully unleash their potential by following these two simple rules: - Before writing any code, write a failing automated test. - Remove duplication. 2. Before the example, the aim is to observe the rhythm of test-driven development: 1. Quickly add a test. 2. Run all tests and see the new one fail. 3. Make a small change. 4. Run all tests and see them all pass. 5. Remove duplication by refactoring. (Footnote: refactoring means changing the internal structure of code without changing its external behavior.) + A reference blog
|
2) Enter the settings — I personally chose Gradle.
3) For this section's options I didn't pick anything.
2. Once the project is created
1) Open the Test class under the test subfolder.
2) First-round requirement:
$5 + 10CHF = $10 (when the FX rate is 2:1) $5 x 2 = $10 <-- let's solve this first, Making amount private Dollar side effects? Money rounding? |
Code: a simple multiplication example. <-- This code becomes the starting benchmark for debugging and refactoring.
public class TddStudy2019ApplicationTests {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10,five.amount);
}
}(1) After writing the first example and running Run As > JUnit, an error popup appears — ignore it. It's because the Dollar class isn't declared yet. The plan is to test first, see the error, and resolve it afterward — part of the TDD flow.
(2) A red bar appears. As expected, the run result is an error. Let's check the error message in the bottom-right.
- There's no Dollar class.
2) Improve the code to address the error.
(1) For now I declared Dollar as an inner class.
public class TddStudy2019ApplicationTests {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10,five.amount);
}
}
class Dollar{
}(2) A red bar appears again. This time the error message changed and a few red squiggles popped up in the test code. But don't panic — it means what to fix is now clearer.
- No constructor The constructor Dollar(int) is undefined
- No times(int) method The method times(int) is undefined for the type Dollar
- No amount field amount cannot be resolved or is not a field
3) Apply the updated error messages.
(1) Improve the code again.
- Add a constructor Dollar(int amount),
- Add a times(int) method times(int multiplier), <-- the book phrases it as 'implementing a times() stub.' (Footnote: a stub implementation means writing just the method signature (and a return statement if it has a return type) so that the (test) code that calls it can compile — an empty shell. In short, 'stub' means a mock implementation: a simple fake that satisfies the interface and can be used in tests.) + Related link
- Add an amount field int amount =10;.
public class TddStudy2019ApplicationTests {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10,five.amount);
}
}
class Dollar{
Dollar(int amount){
}
void times(int multiplier) {
}
int amount =10;
}
3. The first example succeeded.
1) But there's a lot to improve. It parallels the first example in my recent 'Following Toby 3.1 in Spring Boot: Chapter 1 - 1.1 Dreadful DAO' post . Get it running first, then improve the code — that's the book's overall rhythm.
2) Don't forget the generalization cycle before continuing tests.
(1) Add a small test. Done
(2) Run all tests and see the new one fail. Done
(3) Make a small fix. Done
(4) Run all tests and see them pass. Done
(5) Refactor to remove duplication.
If dependency is the problem, duplication is a symptom. Unlike real life — where merely removing the symptom while leaving the cause untouched reveals the problem in the worst places — in a program, removing duplication removes the dependency. Removing duplication before moving to the next test maximizes the chance that one — and only one — code change makes the next test pass.
4. Refactoring. Now to remove the duplication.
1) What can we tweak? Honestly the first example is the author's dramatic setup — the code is a bit odd. Improving those oddities is exactly what the rest of the book does, so don't take it too cynically >< Anyway, the example above is effectively the same as the code below.
In the screenshot, since amount is already 10, I removed the seemingly unnecessary (or confusing) method argument first. That yields assertEquals(10, amount); as the result. Even with the duplication removed it still runs.
But in the story's setup we needed to multiply $5 by 2 — we were just trying to get that test green first, which is why the code looks the way it does. In a sense the code in the screenshot looks more 'correct,' but in real life that kind of requirement wouldn't exist, which is probably why the author chose the example's code. So going forward I'll improve things starting from the code marked '<-- this code becomes the starting benchmark for debugging and refactoring.'
2) With that in mind, first change int amount = 10; to amount = 5 * 2; .
class Dollar{
int amount;
Dollar(int amount){
}
void times(int multiplier) {
amount = 5 * 2;
}
}
3) The rest can be tweaked as below. Both '2)' and '3)' produce a green bar.
class Dollar{
int amount;
Dollar(int amount){
this.amount = amount;
}
void times(int multiplier) {
amount = amount * multiplier;
}
}
4) And that completes one full test cycle.
(1) Add a small test. Done
(2) Run all tests and see the new one fail. Done
(3) Make a small fix. Done
(4) Run all tests and see them pass. Done
(5) Refactor to remove duplication. Done
