Test-Driven Development (TDD): Being the Designer of My Own Code

Louisa Natalika Jovanna
9 min readApr 5, 2021

--

Before you begin reading, there’s something I want to say about TDD: I’m telling you it’s not going to be easy, but it’s gonna be worth it

In this article, I would analogize TDD cycles with the process of design by a designer. First of all, let me tell you about what is TDD.

TDD (Test Driven Development) is an approach where tests created will determine and validate what the code will do. In simple terms, tests are created before code is written. The goal of TDD is to make the code clearer, simpler, and bug-free.

Test-Driven Development starts with designing and developing tests for every little functionality in the app. In TDD, developers can only write new code if the test fails. This approach would avoid code duplication.

So basically TDD is a process where tests are created first, then continued with code implementation and code refactoring.

Sketching: Testing

Sketching

Before producing beautiful works, usually, a designer starts by making a sketch first. Why is sketch important? Sketching can provide an outline of the work you want to create and helps discover the best ideas and solutions to a design problem.

Likewise with the tests that we made. Tests can guide us through our code implementation. With the test, we are required to outline our code so that we do not implement the ‘freestyle’ code. This clarifies the purpose such as input and output of our code. Also, the code that is built based on the outline will certainly be more elegant because we implement it simply. This can prevent us from writing unnecessary complex code and keep our code clean.

Creating tests can also help us to estimate possible edge cases. This ensures that the code being implemented has handled edge cases because this is the only way our code can pass the test.

Creating tests can also save time and energy. WHAT? Are you serious? Yes, I am! By testing, we can prevent useless code implementation. Or, in the fashion designer analogy, we don’t need to color the part outside the sketch line.

Typically, the test stages are marked with a [RED] tag.

Red Tag for Testing Phase

The following is an example of a test for the get bucket lists feature in the application that I am developing, namely Liwat.ID. The Liwat.ID application is a digital travel center application that functions as a personal assistant for tourists. Liwat.ID allows users to easily search for tourist and culinary places, get comprehensive information, create their own itineraries, and buy tickets all at once (bundled) in one single platform. Therefore, at Liwat.ID there is a bucket lists feature as storage for users’ POI (Point of Interests).

I participated in building the bucket lists feature where I was responsible for developing endpoints that returned all POIs in the user’s bucket lists. Although the functionality is small, I still use TDD in developing it.

Creating Tests to Estimate Edge Cases

The code snippet above is the tests I made before implementing the code for the get bucket lists endpoint. I created three tests where each test handles every possible edge case.

First Test: Get Bucket Lists Success

The get_bucketlist_success test is a case where the user stores POIs in his bucket lists. Therefore, I first initialize the user’s bucket list by entering the POI into the user’s bucket lists via the insert_bucket_list() function (the number parameter I input is the index of the POI, bucket list, and user data). After the initialization is complete, I make a GET request to the endpoint that I will hit later, namely the bucket-list endpoint. The bucket list is unique to its owner (in this case the user). That is, the list of POIs returned to the bucket list depends on who made the request. Therefore, there is a http_authorization=self.token parameter where the token is a unique token owned by the logged-in user. After making a request, the server will return a response where the data regarding the returned response will be stored in the response variable. The next part is the assertion that determines whether my test succeeds or fails. Assertions are our expectations as developers when endpoints are developed. In this case, I would expect the server to return a response which status code is 200 which means OK (.assertEqual) and the content of the response contains POIs so the content length is not 0 (.assertNotEqual). The len(response.content) section is used to get the amount of content in the response.

Second Test: Bucket List Empty

Then the bucket_list_no_result test is a case where the user has no POI in his bucket lists. That’s why I didn’t initialize the bucket list to keep it empty. I immediately hit the bucket-list endpoint. Then in the assertion section, I expect that the response returned has a status code of 204 which means there is no content and the length of the content is 0 (empty content).

Third Test: User is Unauthorized

The last test I made, test_get_bucketlist_no_authorized, is a test to handle cases where there are users who are not logged in but hit the bucket-list endpoint. As explained above, the bucket list can only be accessed if the user has logged in. Therefore, in this test, the value of http_authorization is Token notatoken which means that the token is not defined or in other words the user is not logged in. Thus, in the assertion section, I expect that the response returned has a status code of 401 which means unauthorized.

Coloring: Implementing the code

Coloring

After finishing making the sketch, now is the time to color the sketch so that it becomes a work. In software development, the process of coloring a sketch is the same as implementing the code based on the tests we have created. Usually, the code implementation phase is marked with a [GREEN] tag.

Implementing Phase

Code implementation should refer to the tests that have been made. Apart from passing the test, this can help us maintain the KISS principle (Keep It Simple and Straightforward). This can help us to build a clean and single responsibility code.

Get Bucket List Implementation

The code above is an implementation of the test I made in the RED phase. Here is the explanation:

user=request.user

This section will store the user who is requesting the bucket list into the user variable. If the user is unauthorized, the request.user will immediately return Response with a status code of 401 or unauthorized.

bucketlists = BucketList.objects.filter(user=user)

This section will retrieve data about the user’s bucket list by filtering all existing bucket list data in the database based on the user. Therefore there is a .filter(user=user) method. This method will filter the bucket list based on the user (note: the user value in the user parameter is a user variable that stores the user who made the request). Thus, bucket list data based on the requesting user will be stored in the bucket lists variable.

if len(bucketlists)==0:  return Response({      'message': "Bucketlist is empty"      }, status=status.HTTP_204_NO_CONTENT)

This section is a section that will handle cases where the user’s bucket lists are empty or in other words the user has no POI to its bucket lists. If the user’s bucket lists are still empty, then according to the tests that have been made, we will return a response with status code 204 (no content). Apart from that we don’t return any data just return a message that bucket list is empty.

serializer = BucketListSerializer(bucketlists, many=True)

We can’t just return the data about the bucket list. Usually, we will return data in JSON format. Therefore, serialization of the data in the bucketlists variable will be carried out. The many=True parameter indicates that the returned bucket lists may be more than one since the user can store multiple POIs into his bucket list. After the data in the bucketlists has been serialized, it is now in JSON format and stored in a serializer variable to be returned to the user.

return Response(serializer.data)

The serialized bucket lists data on the serializer variable is ready to be returned to the user. Therefore, the data in the serializer will be wrapped in a Response object (we can only return data in the form of a response to the user) and then returned.

Revising: Refactoring

This stage is optional. Not all designs have to undergo revision. Revisions to designs are generally carried out to beautify a work without changing the purpose for which the work was created. Likewise with our code. Not all code has to undergo refactoring. Refactoring is carried out if there is an implementation that can be improved without changing the behavior of the code itself. Usually, this stage is marked with a [REFACTOR] tag, and the code after the refactor must still pass the test without changing the test.

Refactoring Phase

On the get bucket lists feature, I didn’t refactor it because it wasn’t needed. Usually, I refactor to reduce the code smell. Here is an example of the refactoring I did.

Refactoring To Reduce Code Smells

Before, I used to call accounts.User repeatedly on the same file. Of course this is not effective. It will be more effective if accounts.User is only called once and the result of the call is stored in a variable. Therefore, I defined a variable named ACCOUNT_USER which will store the results of calling accounts.User. Then the places that call accounts.User would be replaced with ACCOUNT_USER. Thus the code smells can be reduced because I have reduced redundancy calls.

The only difference between the design step and the TDD cycle is that once you have finished revising the design (if needed), the work is ready to be launched. Meanwhile, in TDD, the Refactoring process is not the end of the story. TDD is cyclic, which means that after refactoring the code there is still a possibility that we will repeat the Testing phase.

TDD Cycle (Source: https://brainhub.eu/blog/test-driven-development-tdd/)

Then, what are the advantages of implementing TDD?

Simple code

TDD implementation can help us to write less complex code. If the current code has passed the tests, we don’t need to add more code. Therefore, TDD can prevent us from writing unnecessary code.

Modular code

In TDD we are encouraged to write unit testing. Unit tests will test individual units of software. This can guide us to implement modular and single responsibility code.

Easy-to-debug code

Tests can help us debug our code when an error occurs. We only need to refer to tests where our code doesn’t pass. It will help us to save our time because we don’t need to do end-to-end debugging of our code.

After experiencing the benefits firsthand, I plan to continue implementing TDD in building applications. TDD can help me to become a better developer because it trains me to think about possible edge cases and helps me to implement clean code. TDD makes me always remember the principle of clean code where the code that is built must be simple, modular, and easy to understand.

I highly recommend other developers implement TDD. Initially, it was not easy to implement TDD. However, if we routinely apply it, of course, we will be familiar and comfortable with the implementation of TDD. Therefore, don’t give up even if TDD feels difficult at first :)

TDD implementation is not easy. I am still adapting to build applications with TDD. However, after experiencing the benefits, I became more and more excited about implementing TDD.

References

https://www.dnnsoftware.com/blog/why-sketching-is-an-important-part-of-the-design -process

--

--

Louisa Natalika Jovanna
Louisa Natalika Jovanna

Written by Louisa Natalika Jovanna

An undergraduate Computer Science Student at University of Indonesia

No responses yet