TDD and Having Fun Coding. and how I finally managed to do both at… | by Travis Kaufman | wITh love | Medium

There Is No Pleasure Without TDD

Muhfathurh
9 min readApr 6, 2021

Imagine you’re a full time software developer, tasked to create a currency trader app that will let customer to exchange its money to another currency with ease. Your manager tells you that it is using Rupiah as its main currency and only allowed exchange between rupiah and another currency. After a lot of development process with your co-worker, you finally finished the app and it was ready for deployment. However, due to the rapid development of your company, an order came from your manager to extend its functionality and made it accessible for another currency to trade with each other. Our current algorithm only took rupiah currency into account, which means currency that has different system, such as yen, dollar, etc. will fail in our current app. Our current solution, modify our code, will make us vulnerable to bugs that can appear anywhere in your code, meaning that you must check every new implementation one by one. Quite a nightmare, isn’t it? (At least for me personally) Altering your code with fear of bugs. Is there any way that we can avoid this nightmare? Here comes TDD to the rescue!

What is TDD?

An obvious way to define TDD is a development approach with test as its basis. By defining them as a basis in our development, we are forced to develop test first. TDD will swap/add (depending on your current development approach) test phases to your development. In a nutshell, my definition of TDD is

Test first, working code later

TDD as a term was first popularized by Kent Beck (Agile enthusiasts surely have heard this name) to create an approach that aligns with agile approach. Agile, as mentioned in my previous article, is a methodology in software development as a response to rapid change in industry. TDD will enhance technical excellence and good design, one of agile principles, by forcing developers to to create a code that is clean. Huh? Clean? What do I mean with that? To answer that, let’s talk about clean code first.

Clean code is a way to write your code in a clean manner, which makes your code more readable and scalable in the eyes of other developers. Readable means that everyone can open your code anytime, read it for a while, and understand your code thoroughly. By doing this, you make the code more scalable, because it is easier for them to develop it even further without changing the existing code (they can just write a new function or line). Just like how you create a document, where a clean and organized document will give others less time to understand it and minimize misunderstanding and confusion, clean code will also gave that, although it is code we are talking about instead of a document. There are several rules that we need to abide in order to achieve clean codes, such as meaningful names, exception handling with try catch and others. How does this relate to TDD? TDD’s objective, is to force you to think ahead and simple by creating tests. Creating a complicated tests are not an easy one, thus, in turn, will create a domino effect to your code, where your code will be created as simple as possible to avoid hard-to-test code. TDD is a way for you to implement one of clean code principles, YAGNI (You ain’t gonna need it) by preventing you from overengineering and create a complicated code, as explained above.

3 is a magic word in TDD

Now that we have discussed about the importance of TDD, let’s understand TDD even further. In applying TDD, there are several rules that you need to comply:

  1. You must write a failing test before you write any production code.
  2. You must not write more of a test than is sufficient to fail, or fail to compile.
  3. You must not write more production code than is sufficient to make the currently failing test pass.

Kent Beck, apply this rule to line-by line basis, where he create a test every time he add new line to his code. However, personally I would create a new test for every function that I made, to create a clear intention what I am going to do (That is why we use function, aren’t we). This rule seems to be contradictory with agile and fast development at first, but as the time goes by and your project get bigger, TDD will save you a lot of development time by making it easier to test your code.

TDD consist of three step that creates mini cycles. This picture below will give you an insight of how TDD works in cycle.

Clean Coder Blog
  1. Red, in this phase, you create a new test for code that we are going to implement. Red is a mark of failing unit test because there are no implementation for your new test.
  2. Green, this phase consist of doing whatever necessary to make you code work, forget about duplication of code, etc. We will deal with it later on.
  3. Refactor, in this phase, you are allowed to make changes to your code, that is much better and cleaner that your implementation in green phase. These include removing duplication of code and make your code more readable according to clean code rules.

Example implementations of TDD

We have learned the basic rules and phases of TDD. However, all of that will become a waste if we can’t apply it to our code. So, in this part, I will give an example of how I am applying TDD to my code. Let’s begin!

In my team’s project, we are asked to create an authentication through University of Indonesia’s SSO(single sign on). To achieve that, one of the most important component that we need is a signal. Signal allows decoupled application (such as database) to receive a “notification” when actions occur somewhere else in our framework (in our case, SSO login). My idea is, we can create a new user in our database right after SSO login. This, will prevent duplication of user data and create a new user data with the information from our SSO login. With that in mind, I will try to create my code first.

from django.test import TestCase
from django_cas_ng.signals import cas_user_authenticated

from .models import User


class SignalsTestCase(TestCase):
def test_signals_send(self):
instance = {
"ldap_cn": "MUHAMMAD FATHURIZKI HERLANDO",
"kd_org": "01.00.12.01",
"peran_user": "staff",
"nama": "Muhammad Fathurizki Herlando",
"npm": "1806205496",
}
cas_user_authenticated.send(
User, attributes=instance, username="muhammad.fathurizki"
)
self.assertEquals(User.objects.count(), 1)

def test_signals_send_already_created(self):
User.objects.create(
email="muhammad.fathurizki@ui.ac.id",
npmnip="1818181818",
name="fathur"
)
instance = {
"ldap_cn": "MUHAMMAD FATHURIZKI HERLANDO",
"kd_org": "01.00.12.01",
"peran_user": "staff",
"nama": "Muhammad Fathurizki Herlando",
"npm": "1806205496",
}
cas_user_authenticated.send(
User, attributes=instance, username="muhammad.fathurizki"
)
self.assertEquals(User.objects.count(), 1)

In this test, I will utilize my framework (Django) test class, to create a new test and leave all things besides test such as preparation, to my framework test class by creating a subclass from it. In this test, a signal named cas_user_authenticated will be created every time SSO login is successful and send its data to our user database (For futher understanding on how django cas works, read here). In my test case, I’ll try to mimic a successful SSO login by send its signal manually to the database. There are several data that we need to passed on, however I prefer it simple and just send data related to our user creation.

After we are done with creating our test, create a new commit to our git repository. This will create a fail/red deployment in our app, but, it is a sign that our test have not been implemented yet in our code.

My commit for tests in gitlab

Our next part is the most fun part, code our implementation. How the hell coding is a fun part?? Well, in the previous part we have created a test and thought carefully of our implementation in a simple manner. Now, we can just skip the part of searching for an idea an research in our code, and get straight into our beloved code editor application. In our SSO login signals test, this is how I create my implementation.

import json

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.dispatch import receiver
....
from django_cas_ng.signals import cas_user_authenticated
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

from .models import Mahasiswa, User
....

....

ORG_CODE = {}
with open(settings.SSO_UI_ORG_DETAIL_FILE_PATH, "r") as ORG_CODE_FILE:
ORG_CODE.update(json.load(ORG_CODE_FILE))


@receiver(cas_user_authenticated)
def save_user_attributes(sender, **kwargs):
args = {}
args.update(kwargs)
attributes = args.get("attributes")
username = args.get("username")
try:
user = User.objects.get(email="{}@ui.ac.id".format(username))
except ObjectDoesNotExist:
if attributes["peran_user"] == "staff":
user = User.objects.create_user(
email="{}@ui.ac.id".format(username),
name=attributes["nama"],
npmnip=attributes["npm"],
)
user.save()
org = ORG_CODE[attributes["kd_org"]]
Mahasiswa.objects.create(
user=user,
prodi=org["study_program"],
angkatan=attributes["npm"][0:2],
)

In this implementation, I utilize two table for my user data. This is because we have several user types in our web application, and unfortunately in Django implementing it is a complicated one. A roundabout way for this is to create a one to one relationship between user and user types database, as shown in my code above.

After we are done with our implementation, we should test our code locally and make sure that our code is working. In my case, although there are no screenshot, it is working like a wonder, and… voila! We have created an implementation with TDD approach. Let’s create a commit in our git repository and tells our friends or co-workers that we have implemented a small part of new functionality in our app.

A gitlab commit for green phases (Seeing a green circle is refreshing, isn’t it)

The next and last phase of TDD approach is quite optional. Refactoring is a thing that we do when we feel there are better implementation of our code that comply with clean code approach (especially, DRY part). In our SSO case, upon reading django-cas-ng library source code carefully, I have conclude that using a username, instead of an email is way better for our user database, especially considering we are using User class from our django framework that has username as its username field. Thus, I will create some changes to my previously working code.

Changes that I made in when I refactor my code

After we verify that our code is still working through unit test, let us push our code to our gitlab repository.

An example of refactoring commit

Ahh… Finally we have reached the end of our TDD phase. By creating a test first, we forced ourselves to create our code in a simple manner and understanding the concept of our soon to be implemented code. Also, as I have shown before, refactoring codes after our first implementation is an easy one when we use TDD approach. For me personally, this is one of the most useful part in TDD approach that make us more agile to the rapid changes in industry. I suggest Ken Beck’s book: Test Driven Development by Example and Harry J. W. Perceival’s book: Test-Driven Development with Python_ Obey the Testing Goat_ Using Django, Selenium, and JavaScript for further readings, with Ken Beck’s book as a more generalized one. That is all for me and have a nice coding!

--

--

Muhfathurh
Muhfathurh

No responses yet