My Take on TDD
August 21, 2021
Roughly ten years or so ago when I was working on Java 1.5 projects I was taught that testing was a “nice to have” and not a major concern. Although this always felt wrong, I hated writing tests and usually just went a long with it. We would write code, test it manually on a test server and then release it into the wild, and that generally worked for us.
I always had a bad feeling about it, but I was a junior at the time working directly with a CTO and some senior devs, so I wasn't about to complain.
If anything went wrong it was kinda their problem..
Unfortunately for me this left me with some bad habits. I found my self consistantly writing logic, and pushing my changes only to have my manager message me with "where are the tests?". TDD has helped me get out of this habit.
The idea behind it is by writing down the tests first you can use them essentually as instructions for you logic.
General concept
The general idea behind test driven development is to write tests before doing the actual development. The thinking is that this will get the programmer thinking about what is actually required in a broad sense, and thus using the tests to drive the logic development.
The official process is:
- Add a test
- Run all tests to check if it fails
- Write some logic
- Run the tests
- Refactor
- Repeat
And thats just great!, In my experience most software practises should't be taken so seriously and can be slightly adjusted, which is what I have done.
Add A Test
add a test is not as easy as it sounds, you can spend a long time on this point. In my opinion the best place to start is with the test description, a good description can make writing the test a lot easier.
Lets say I have been tasked with writing a pallendrome function, the function will take some input and return a boolean value, true if the input is a pallendrome and false if it isn't. Lets write some test signitures for this function:
it('should return true when input is a pallendrome');
it('should return false when input is not a pallendrome');
it('should return true when input is a sentance and is a pallendrome');
it('should return true when input is a pallendrome regardless of casing');
By writing these signitures we have already defined the shell of our logic, we know the input may be a single word or a sentance, we know the casing should't affect the outcome. These scenarios could have been an after thought if we wern't implimenting TDD.
Run all tests to check if it fails
This step always seems pointless to me. If I was doing TDD the way The Man wants me too I would have added some logic to my tests. Personally I don't see the point, it will fail as I dont have the actaul logic yet. In some cases I might have the logic already written, i.e. if I was patching something etc, in that case I would have added logic, but when adding something new I usually skip this step.
Write Some Logic
Woooop this is the best bit! And thanks to our test signitures we have a good idea of what it is we need to do, we need a function which returns a boolean, true if the input is a pallendrome, false if it's not, ingore casing and handle sentances.
export class PalindromeChecker {
function isPallendrome(input:string): boolean {
return str.split('').reverse().join('') === input;
}
}
Run The Tests (Write The Tests)
This step changes to Write The Tests as we dont actaully have any logic in our tests at the moment, but now we have a class PalindromeChecker
and a function isPallendrome
to test against.
it('should return true when input is a pallendrome', () => { ✅
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('dad')).toBeTruthy();
});
it('should return false when input is not a pallendrome', () => { ✅
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('date')).toBeFalsy();
});
it('should return true when input is a pallendrome regardless of casing', () => { ❌
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('Dad')).toBeTruthy();
});
it('should return true when input is a sentance and is a pallendrome', () => { ❌
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('do geese see god')).toBeTruthy();
});
The first two tests pass and the last two fail because we don't yet have the logic for ignoring casing and sentances
Refactor
Now we go back to our logic and refactor. It's important we dont try to fix all the tests at once, much more simple if we take the first faling test and refactor the logic to make it pass and then go back around for the next test.
export class PalindromeChecker {
function isPallendrome(input:string): boolean {
const result str.split('').reverse().join('');
return result.toLowerCase() === input.toLowerCase();
}
}
So now we're reversing the string and storing it in a varible named result. We are lowering the casing on the result varible and the input string and then returning the output of comparison.
Repeat
So now we run the tests again:
it('should return true when input is a pallendrome', () => { ✅
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('dad')).toBeTruthy();
});
it('should return false when input is not a pallendrome', () => { ✅
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('date')).toBeFalsy();
});
it('should return true when input is a pallendrome regardless of casing', () => { ✅
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('Dad')).toBeTruthy();
});
it('should return true when input is a sentance and is a pallendrome', () => { ❌
const palindromeChecker = new PalindromeChecker();
expect(palindromeChecker.isAPalindrome('do geese see god')).toBeTruthy();
});
Woooop a previously failing test is now passing! So now we repeat by adding more logic to isAPalindrome
function to get the last test to pass.
ta da!
Conclusion
Previously I found writing tests to be a bit of a pain, but with TDD it's actaully enjoyable. Writing logic to get the tests to pass is fun, where as writing tests against logic that already exists is not.