An Intern’s Experience of Using Test Driven Development to Fix a Bug (Ruby on Rails)

Felicity Boey
Fave Product Engineering
10 min readSep 9, 2021

I’m a fresh graduate and software engineer at Fave, and I previously spent three months here as an intern working on the backend. A task assigned to me during the first month of my internship was to fix a bug that an external party had found when calling one of Fave’s API endpoints.

This article is going to cover my experience of reproducing and fixing the bug using Test Driven Development, as well as what I learnt along the way. (As Fave’s backend is built with Ruby on Rails, the article will cover Ruby specific tools such as RSpec, Byebug, and FactoryBot)

Stage 1 — Where to Begin?

When I was first assigned a ticket to fix a bug that was discovered from calling an API endpoint where a value was returned in an unexpected format, I thought I was in over my head. First of all, I was just an intern; what did I know about fixing a bug in some code that someone else wrote? The answer to that- nothing! My only experience in fixing bugs is with bugs found in my own code, so… bugs that I wrote myself. How was I going to be able to even find the bug, let alone fix it?

I started off by reading through the ticket, which stated the file where the value was declared. I understood the description of the ticket and the task, and knew what had gone wrong, but had no idea what to do next to fix it. Long story short, the bug was that a value returned when calling an API endpoint was in a date time format, rather than in an iso8601 date format. It was my job to find out why, reproduce the bug locally, and fix it.

Stage 2 — Gradual Understanding

I combed through the codebase looking for any parts of the code that might be related to my task to get more of an understanding of what was happening, and to see if I could figure out the next step. I read up on the API documentation and looked at the sequence diagram to get an idea of the flow of how the API worked, and had more context on what was going on.

By this point, I knew where the value that was returning an inconsistent format was coming from, but I wasn’t sure what to do with that information. As a start, I went into the rails console to print the value to see what was being returned, which is where I saw that the value was in a date time format rather than just a date. This was how I was sure that I had found the problem, but I wanted to double-check before I assumed anything.

I needed to check the exact line of code that I felt was causing the problem so it was necessary to carry out some debugging. An easy way to debug is with “puts debugging” or “printf debugging”, where you print out the value of the variable that you want to debug. Another way to debug in Ruby is using Byebug, a debugging gem, that lets you stop the code from executing wherever you place the “byebug” statement, run the code line by line, find out the values of different variables, amongst other functions. I used Byebug by placing it after the line that I suspected was causing the problem, and inspected the value of the variable. To my relief, the value really was being returned as a date time, so now I could confidently say that I knew where the bug was.

I did some Stack Overflow-ing to see how to make that variable return as a date, added the changes, and thought I was good to go. At this point I had a meeting with a member of my team, Esh, to have an alignment on the task. I told Esh that I was unsure of how to see if the changes I made had an effect on the API, which was when he gave me some advice to write a test to call the API and see whether my changes had worked.

Stage 3 — Debugging and Writing the Test

A diagram showing the continuous flow of Test Driven Development: ‘Write a failing test’, ‘Make the test pass’, to ‘Refactor’
The continuous flow of TDD (further explanation can be found here)

This is known as Test Driven Development (TDD), where you write a test before you write the code, and the test will fail; then you write code to make the test pass, refactor the test, and repeat the process of writing code that fails, getting it to pass, and refactoring. Usually with TDD, for any code that you write, you will also write a corresponding rspec file (rspec is a Ruby testing framework) to test the code so that you can prove that it works, and so that it’s easy to notice if it breaks. This was my first time carrying out TDD, so Esh gave me a brief summary of how to apply it to my situation — write a test that fails (without changing the original code), fix the bug, then change the test to make it pass. Red dots, fix bug, green dots, refactor.

Red dots, fix bug, green dots, refactor.

That was when I began to have more of an idea of what I had to do. Run the rspec of the original code, add Byebug after the line of code that is causing the problem, and run the rspec again, to see what kind of values are being returned. After that, write an rspec to ensure that the value will be returned as a date time, change the problematic code, then change the rspec and expect the value to now be in a date format. Easy.

However, this ended up being harder to implement than I had thought, seeing as I had never written an rspec before. In the beginning, I didn’t really know what values should be expected, because I wasn’t sure what kind of values would be returned in the first place, or what the response of the API would be.

I decided to just try out creating the test, then attempted to add some dummy data to it, but hit a wall here because I didn’t know what type of data to add — It’s hard to make up fake data when you don’t know what should be there in the first place. I mean… what does a QR code look like? So, I followed the example in the API documentation in the hopes that it would work.

When I was running the test to call the API, I was getting some disheartening responses, status code 500 — internal service error. I didn’t know what I was doing wrong, or which part of the code was giving me issues. I raised these concerns to Esh, who told me about using factories to create data that will be used for the test. With the use of a factory, the FactoryBot gem to be specific, I would be able to create objects with values that could be then used in the test. For instance, I could create a QR code object and just call it when needed, rather than making up a value and hoping that it would work.

I went about creating factory objects and adding some headers and parameters, and then finally had a breakthrough — some meaningful errors found in the logs. I was able to see what was going wrong, and gradually created more objects, changed some parameters, added some headers, experimented with Byebug, and one by one fixed each problem.

At long last, I was finally able to reproduce the bug, where my test was expecting the date to be in an iso8601 format, but failed as it was returned as a date time. Hooray, halfway there!

Stage 4 — Refactoring

I was now at the point where I had gotten the test to fail for the reason I wanted, now it was time to fix the bug, and change the code to make the test pass. So that’s what I did, I fixed the bug, ran my test again, surely it will work now? Red dots again… for a different reason this time. One day of frustration and a session with Esh later, we realised that I had made a mistake and was putting incorrect values in the parameters. We changed the parameters, and now — green dots, a passing test! All that was left now was to push the code to GitHub and create a pull request for my colleagues to review the code.

After that, I refactored the test according to some comments left on the pull request by Esh. I had some repeating code in the test so I removed it and used a shared example instead.

Shared examples are used to make your code more DRY (Don’t Repeat Yourself), a principle used to reduce the repetition of code. This means that the same code will not be repeated multiple times, but is instead written once and called by the areas of the test that need them. By using shared examples, it reduces duplicated code, and makes it easier to change the test if needed, as only the shared example would require changes, therefore reducing the need to trawl through the code to find each area to be changed.

I also learnt the difference between using ‘let’ and ‘let!’ when writing tests. ‘let’ is a method that creates a return value that can be used throughout the test, and is evaluated only when it is called the first time. On the other hand, ‘let!’ (with a bang/ exclamation mark) evaluates and caches the return value before each test example.

Once I had fixed the small mistakes and refactored the test, I was almost done! The bug had been recreated locally, found and fixed, an rspec test had been written to prove that the call to the API endpoint resulted in the correct response, and my pull request had been approved by my teammates.

Stage 5 — Communications, Final Testing, and Deployment

Now for the next step, which was to deploy the changes to a staging server to test the API call there. I used Postman, an API client, to call the API and was able to verify that the value was returned as a date as hoped.

Meanwhile, the project manager of my team was also able to verify that the changes were working by testing the feature that called the API on the frontend. He also asked the external party that discovered the bug to carry out their own test on the API from their end, to which they confirmed that the fix had worked.

Now that I had gotten the green light that everything was working fine, I deployed to production and had completed the task. And with that, there was my first experience of fixing a bug found in production during the first month of my internship.

Tools Used

When working on this task, the editor that I used was Visual Studio Code, an integrated development environment (IDE) that has syntax highlighting and code completion features amongst others. I installed a useful extension called GitLens, that allowed me to visualise my Git commits so that I could see what changes had been made with each commit.

As the backend codebase is written in Ruby on Rails, I was able to gain more familiarity with Rails, as well as with using the rspec framework to create tests. To begin, I made use of the rails console to interact with the application from the command line, and discovered the extremely useful functionalities of Byebug.

For communication amongst myself and my colleagues, Slack, the platform that the company uses to communicate internally, was used for discussions, as well as Google Meets that enabled us to have face-to-face meetings to work on the task when I needed help. When I needed a review of my code, I created a pull request on GitHub and tagged my colleagues as reviewers; after they had left feedback, I was able to see where I had gone wrong and the areas of the code that could be improved.

Once it came to deploying the code to the staging server (followed by production once tests had been completed), I used ArgoCD, a tool that continuously monitors running applications, to check that the code containing my changes had been deployed.

Learnings

Throughout the whole process of being assigned the task, figuring out what and where the bug was, to learning about TDD, writing my first test, fixing the bug, and deployment, I had learnt so much in a short amount of time.

I learnt about the basics of TDD and how it’s used, as well as experienced using TDD first-hand. I discovered the usefulness of using a debugger, in this case, Byebug, and how it works better than printing out variables and hoping for the best. I also learnt about using factories to create objects that can be used in tests, discovered how helpful logs can be, and was reminded of the importance of checking for typos and little mistakes. Another thing that I had learnt was the process of refactoring, and the things that make a test better, such as shared examples and the descriptive naming of test cases.

It was a good learning experience to deploy my code to a staging server for the first time and to figure out the process of testing it on Postman, as well as the deployment to production once all of the testings finished.

All in all, I learnt a lot while completing this task, and I’m glad that I was able to document the process and hopefully help someone else along the way.

Helpful resources for Byebug

--

--