TDD not so easy, not so hard
Every time I talk with people about TDD, the main topic is the difficulty of doing it well. I use to say that it’s no so easy as some people say it is, but also, is not so hard either.
From my point of view, you need to have 3 things:
1- To have a clear and well defined functionality: someone have to define the behaviour you have to code. It includes defining how it should behave when it works fine and also when it fails in any way it can.
2- To have a good level of envolved technology: you can’t make TDD if you need to use technology you are not used to work with. In that case, maybe you should make a POC project first.
3- Knowing how and what to test: understand how to check values and behaviours, how to set an environment for the test and how to validate that the test code is passing through the required production code.
To show you this “hows”, I will show you how I’m used to do it with Node.js.
First, let’s add a new functionality to an existing project.
a) The required task to accomplish it, is to add a new endpoint /country/:name/capital
that:
- receive a country name
- call
https://rescountries.eu/rest/v2/name
service and save in data base the country name and capital - if the country name is already in the database, calling to a service is not required and it should be returned from it
The first step is to check if we have the functionality well defined and it covers all the possible situations.
As we see, the requirements define what to do when it goes well, but not what to do when it fails. Let’s add one more requirement:
- if the name is not in database and neither it’s found calling to service, it should return an error 500 with { code: 500, message: “Country not exists” } message.
- if service returns an error, we should handle the error and response an error 500 with { code: 500, message: “Service not available” }.
To accomplish the task, we just need to call an external service and save and read data from mongo. We already know how to do both task so no POC project is needed in this case.
We assume we already made a lot of test and we know what, when and how to test or code.
Let’s start with the code.
As you can see we defined the case (just one case) and we only have the base code for our endpoint(you can even start with no code or file for your endpoint).
We implement our first test code:
Explaining a little bit our code we can see that:
- where we have
const app = require('../../');
we are requiring ourindex
file and running our server(we have an API and we want to test our endpoints) - for all the test in this file, we are connecting and disconnecting to our database in test (a in memory mongodb instance)
- we are preparing our “scenario” for the
GET /contry/:name/capital
test in thebefore
block and ensuring to clean it in theafter
block. - we run the code (in this case an http call to our endpoint) in the
before
block and then run several test in differentit
blocks. - we have to check that our database, after the execution, has the correct data saved.
- we also validate that the response from the service has the correct data.
If we run this test npm run test
we are going to get the expected errors.
Now we are going to write only the code to pass the actual test.
And now, the test will pass
We only wrote the necessary code to pass the actual test. TDD claims that you have to:
- write test
- see the test fails
- write the code
- see the test pass
- refactor(if needed)
- write more test….
The actual code at this point is here.
Let’s add new functionality: “if the name is not in database and neither it’s found calling to the service, it should return an error 500 with { code: 500, message: “Country does not exists” } message.”.
First, the test:
As we can see, the test fails again:
Now, let’s implement the code to pass the failing test.
At this point, the actual code is here.
By now, we are not going to make any refactor, so keep add new functionality. Next step is to handle errors: “if the name is not in the database and neither it’s found calling to the external service, it should return an error 500 with “Country does not exists” message”
As always, we start writing the test
And once again we have the test failing.
Let’s add the needed code to pass the test case. You can see the actual code here.
And finally, add the last test case “if the external service returns an error, we should handle the error with a response error 500 with { code: 500, message: “Service not available” }”.
Again, the test are failing now
Finally, let’s add the required code and now, the test are passing. The code at this point is here.
Now, you can refactor some code to make it more easy to read or even to improve the performance.
First, extracting some code in if
statement to its own function
See the refactor in this commit.
Here, we are to use Promise
chaining instead of async/await
(in this case I think it’s easy to read this way)
See the complete result in this commit.
OK, we have completed our task and maybe it took some more time that it would take if we don’t write any test (because you can think the task is quite easy and it does not worth it) but , we can go home and sleep peacefully.
Now, someone comes and say to us that when we don’t have results in database or in the service, we should return a http code 404 and the error message { code: 404, message: "Country not exists"}
. With our previous test, it is quite easy and fast. Let’s see.
First, change the test case
Just a couple of changes and now, we see the test failing.
Let’s make the required changes in the code
Now, the code has changed to adapt the new requirement and all tests still pass. It was quite easy and most important, save, and we can say that we don’t add new bugs to our existing code!.
NOTE: For this example, we don’t make the test for the service layer, we just test the “controller” layer, but, as we are not mocking nothing, we are testing our services too at the same time.
This is the actual coverage report:
(we have not 100% in controllers because we are not testing the example endpoint for monit in the app)
OK, this was a very easy example for TDD, but using this way of writing code and having the 3 key things (clear and well defined functionality, good level of envolved technology, knowing how and what to test) under control, we can develop any code applying TDD.
The most you try this way of writing code, the easiest it becomes.
I hope this will help you improve your way of writing code and improve the quality of the delivered code.
You can see the complete project here.
Feel free to leave any comment and thanks for reading.