Why we use TDD or Test Driven Development (and you should too).
Writing code for a larger piece of software comes with many challenges that can be solved with different software development principles and techniques. For me, the most simple of these, and the one that is absolutely essential for any application backend is Test Driven Development (TDD).
TDD is the process of writing unit tests to shape your production code, rather than writing production code and then testing it. I won’t go into detail about why you should unit test your code (i’ll do that in another article soon), but I will talk about the benefits of using TDD to write a bulletproof backend.
The TDD workflow can be a difficult one to get started with as it’s backwards to how you’re likely to have worked previously. It can often feel like you’re spending longer doing it than simply testing your code after you’ve written it. But as with anything in life, practice makes perfect. Once you get into the flow of it you’ll actually find yourself writing higher quality code much faster than you ever have before.
The main benefit of TDD is clarity of the problem. Often seemingly complex problems have the simplest solutions and we see developers (I’m guilty of this too) coding themselves into a corner. Complex problems drive complex solutions which can be a problem. Jumping straight into the code will result in you writing lines and lines of complex code that no one (including you) will understand in 3 months time.
Instead, taking time to break the problem down into smaller problems will make it much easier to tackle. Ultimately it will result in you writing higher quality code. With TDD you write out each unit test before the code so you can write a blueprint of exactly how your code should work. The way I do this is I write out my test names and then a comment of each step of the test:
/**
* Problem: Should allow a user to update their password
*/
test('it should allow a user to change their password with a valid token', (t) => {
// Create a user
// Create a valid password reset token
// Request to update password with a new, valid password
// Assert that the users password was set (compare hashes)
});
test('it should prevent a user updating their password with an expired token', (t) => {
// Create a user
// Create an expired password reset token
// Request to update password with a new, valid password
// Assert that the users password was not changed
// Assert a useful error was thrown
});
test('it should prevent a user updating their password to the same as their current password', (t) => {
// Create a password
// Create a user with the password
// Create a valid password reset token
// Request to update password with the same password
// Assert a useful error was thrown
});
...
This approach allows me to have a clearer understanding of exactly what’s involved in the initial problem and how to solve it. By the time I come to write my production code I already know the exact steps I need to take to achieve the end goal and I will sometimes even follow the same approach I use to write my tests for production code:
const updatePassword = (_, { password, passwordConfirmation }: PasswordUpdateInput, context: Context): SuccessResponse => {
// Validate password reset token
// Validate the passwords match
// Validate the new password is not the same as the old one
// Update the current users password
// Return a success message
}
There are a lot of other benefits to TDD but having clarity of thought when working on larger backend applications is crucial and to me is a good enough reason for anyone to at least try out the TDD workflow so next time you have a problem to solve, try out TDD for yourself and see what you think.