TDD: A reflection

Dave Nathanael
5 min readApr 15, 2020

In my PPL (software development course) group project, we’re enforced to follow the test-driven development guideline throughout the project time frame. That is, to write tests first, do a minimal implementation that passes the test, and tidy up and refactor the implementation (so its a bit nicer). Everyone in my team follows TDD, either its a code for the web, mobile, or backend project. We have a CI pipeline up and running on our faculty instance of GitLab every time we pushed a commit and we’ll label our commits by [RED], [GREEN] , and [REFACTOR] prefix on the commit message.

Personally, this is the first project that I ended up using TDD during the whole project (well, its because of the course requirement/enforcement, but to be honest if not for the course requirement, I would never started to do TDD on a regular basis haha). I will describe how I perform test-driven development on this project and I will write my reflections/key takeaways of using TDD based on my experience on this project.

How I do TDD

Red

I think of which feature to implement, how will I organize the implementation (in classes or functions), how will I write the code (for each class/function), and then I break it up into different test cases for different scenarios I have in mind. I feel like this step is the most important of all, since test should be limiting our implementation in the green phase, I need to have well-thought test cases that can be easily transformed into implementations later on.

In the below commit diff, I wrote a test for ensuring Account creation results in the creation of a Log record with appropriate attributes (its a creation log for Account of id x).

[RED] Add log integration tests on Account test cases

Green

On this phase, I write minimal implementation that pass the newly written test. Note the minimal, it is recommended to write just enough code to pass the test written, not more, so that we don’t end with untested code.

I implemented the same feature: creation of Log object when new Account is created, on the following commit diff.

[GREEN] Implement log integration on Account view

Refactor

This is where I clean up the previous implementation, code style-wise. I can’t change the functionality, just to tidy up or organize better. I can set a better variable name, extract repetitive lines of code, simplify algorithm, or anything that don’t change functionality, since in the green phase our main goal is just to pass the tests.

I clean up bad variable names and replaced it with more straightforward and explainable names.

[REFACTOR] Add new constants for each model names and activity types

Key takeaways

Steep learning curve

To commit on test-driven development means you’ll have to write test strictly before you write the actual code. That means you have to have a fairly decent understanding of the actual code that you’re about to write and how are you going to test it. You have to know the testing library, testing constructs like stub and mock, what to test, how to actually write (good) tests, etc. Perhaps you don’t need to write the perfect test but you’ll want to have good tests. After all, the tests purpose is to prove your code works as expected, thus you need to have good tests to be able to prove it.

You need to be fairly good at the main code (product implementation) and also at the tests. That’s an extra thing to learn and be good at, and you’ll write a lot more tests than the actual code. For me, TDD has a steep learning curve until I can produce good quality tests along with good code within a reasonable time frame.

More code

I wrote a lot more code than if I don’t practice TDD. Especially with our choice of our backend service framework: Django Rest Framework (DRF). A lot of things has been handled “automagically” by DRF and compared to other frameworks, I can write a lot less and achieved a lot more with DRF and its additional libraries for various useful features. Of course, the framework is tested and I do not need to test the framework.

I still have to test my own code such as my helper functions and endpoints, and that requires a lot more code than the actual implementation (which usually only needs a few lines of code, because DRF). We do not stop at 100% code coverage but also consider the different scenarios and cases a certain endpoint/function can meet so we test them too and it adds a whole lot more tests.

More confidence

At one point of time we need to modify an existing implementation because of some updates in our product requirement that also includes removal of a model. It’s a fairly risky action to do. We need to be aware about possible unexpected changes on other parts of the code when we do the modification/removal. We don’t want to accidentally break other parts because the modification on one part. That’s why we have tests to help and at some time, tweak the affected tests that are not needed/not relevant for the current state of the code, so we can ensure that the other parts still work just fine.

Presence of tests also give us more confidence when we publish new pull request with new changes and also when we put our product in production environment. There are a lot of things to be tested and manual would be impossible. Thankful we do TDD so we have well-used tests.

There are sure still a lot more things to learn from my experience on test-driven development in this project, but so far I feel these points are the most impactful for me.

This article is a part of my writing series about software development that cover topics such as Git, Agile framework, TDD, Clean Code, CI/CD, and many more.

--

--