“Bad programmers have all the answers. Good testers have all the questions.” – Gil Zilberfeld

What is Test Driven Development?
One of the things that was always really hard for me to implement was Test Driven Development. Fundamentally, it is a development method where you write the tests first, before the actual solution is written. This used to be the only way to develop software, as elaborated by Kent Beck, an American Software Engineer and the creator of Extreme Programming in the quote below, which you can also find in the book “Test Driven Development: By Example” which he is the author of:
The original description of TDD was in an ancient book about programming. It said you take the input tape, manually type in the output tape you expect, then program until the actual output tape matches the expected output. After I’d written the first xUnit framework in Smalltalk I remembered reading this and tried it out. That was the origin of TDD for me. When describing TDD to older programmers, I often hear, “Of course. How else could you program?” Therefore I refer to my role as “rediscovering” TDD. – Kent Beck
It involves five steps, as illustrated by the beautifully made diagram at the start of this blog post:
Adding the test is the first thing that is done whenever a feature is added. The test that is written describes what needs to be implemented. To create a good test, the developer must understand the features that needs to be added and the specification of the code that needs to be written.
The test that is added could either be a completely new test, or it can be a modification of the old test. There may be a time where the test that is written allows poorly written, or “hacky” code to pass. Old tests should be modified as required so that it will not pass codes that are not written properly.
-
Run all the test to see if it fails
After the test is added, all the tests need to be run. This is to make sure that the test harness (collection of frameworks and data that is used to test a program by monitoring its behavior and outputs) works properly. What is meant by this is that the test cases should not allow the existing code to pass without modifications.
-
Write the bare-minimum code that solves the test
After we have verified that the test harness is working as it should, it is now the time to write some code. In this step, you should keep in mind that the code that you write should always be the minimum code required to pass the test. At this stage you may modify the existing code, but you must make sure that all the tests return green before proceeding to the next step.
This is a step where the code is checked for cleanliness. Parts of the code may be moved, or modified to improve the quality, readability and maintainability of the code. I put an emphasis on may, as the code may not need to be modified. This usually happens when the code base is still tiny. However, as the code base increases in size, it is more likely that the code needs refactoring.
The steps above is repeated until all the functionality that is needed are implemented.
Unlike the Development Driven Testing process of creating an application, where features could be added as you code, and tests are created based on what you have implemented, Test Driven Development leaves very little room for modifications in terms of design, so you have to thoroughly analyze what it is that your code has to achieve, then create test cases for each of the things that it needs to achieve, before actually coding it. It’s like a mini-waterfall process so to speak.
This may feel tedious at first, but there are numerous benefits that come with TDD. While I will elaborate the benefits that I felt later on in the text, I feel that the benefits that I experienced when compared to other developers will be different since the project that I do is small in scale and has a very short development time of 5 sprints where each sprint is done in a two week time frame, so you may experience more benefits if you developed software in a longer time frame than I.
The Test Driven Development Process in Our Course
If you don’t know, this blog post is created because it is required for one of my courses, which is PPL. My course has standards regarding how we do test driven development, so I’m going to elaborate on that first before telling you how I do it.
There are three types of commits regarding the implementation of required features. They are:
- [RED]
- [GREEN]
- [REFACTOR]
[RED] means that your commit adds the test that needs to be passed by the code. [GREEN] means that your commit will include the bare-minimum of code needed to pass the test on [RED]. [REFACTOR] means that your commit will include changes of the code after it has passed all the tests that it needs to pass. The refactor commit need not be used if the code does not require refactoring.
How I Do it Based on the Requirements Elaborated Above
Since it is not as complicated in the front end, it is not that hard for me implement good test driven development principles for the features that I need to implement.
The first commit that I will do is [RED]. I may do multiple red commits depending on whether or not I feel the test is already correct. I may come up with cases that I have not written as a test after I committed and because of that I do multiple commits of red.


Then I pushed the [GREEN] commit that implements what is tested. I usually pushed this to the online repository after the tests pass on the local environment, so that it has a higher chance of succeeding in the pipeline.

After the [GREEN] commit, I can refactor the code. As I said before, this is an optional task, which means that if you think that the code does not need to be altered due to the fact that when you write it, it is already good enough, you can simply merge this with the other branches.
Oftentimes I do not need to refactor the code since the code I wrote is well written. However, for complicated components, or when you are adding features to existing components, you may need to refactor. I do not experience this often due to the fact that I am creating new components as I go, however your mileage may vary.
Examples of What I Wrote.
One of the tests that I wrote is as follows. This is for the small component that contains an input form, and two buttons, “plus button” and “minus button” defined by what symbol is on the button, which are the only ways to increase and decrease the value of the input form. The value of the input form cannot be subtracted to be less than one, and the value can be added indefinitely.
import React from 'react';
import { unmountComponentAtNode } from "react-dom";
import { shallow, configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import JumlahSesiPerHariComponent from './components/settingPage/JumlahSesiPerHariComponent.jsx';
configure({ adapter: new Adapter() });
let container = null;
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
describe('JumlahSesiPerHariComponent Functions', () => {
it('Plus Button should add jumlahSesi', () => {
const tree = shallow(
<JumlahSesiPerHariComponent />
);
tree.find('#plusButton').simulate('click');
const instance = tree.instance();
expect(instance.getJumlahSesi()).toBe(2);
});
it('Minus Button should subtract the jumlahSesi', () => {
const tree = shallow(
<JumlahSesiPerHariComponent />
);
tree.find('#plusButton').simulate('click');
tree.find('#plusButton').simulate('click');
tree.find('#minusButton').simulate('click');
const instance = tree.instance();
expect(instance.getJumlahSesi()).toBe(2);
});
it('Minus Button should not subtract the jumlahSesi if jumlahSesi is one', () => {
const tree = shallow(
<JumlahSesiPerHariComponent />
);
tree.find('#minusButton').simulate('click');
const instance = tree.instance();
expect(instance.getJumlahSesi()).toBe(1);
});
});
Notice that with a small, small featured, front end component such as this, we are testing functionally the things that a user can do, which are to press the “plus button” and the “minus button”. This is why the code of the test can be significantly longer than the actual code that is done to implement the features, which you will see later down below. Before that though, I want to describe to you what the test cases that I have written will do to prove that it is not just tests that are arbitrarily written to get the coverage up to 100%.
The first test creates a shallow copy of the component. As coded, the component starts with one as the original value of the form. It is put inside a state. The first test modifies that by simulating a click on the “plus button”. When the value of the form is checked, it should be two, or else the test will fail.
The second test we start over, then simulates a click on the “plus button” twice to make the value of the input form becomes three, then simulates a click on the “minus button” so that the value decreases into two. Again, the value of the form is checked; it should be two, or else the test will fail.
The last test checks whether or not the button subtracts when the value is one. It should not. The way that this is done is by starting over with the original component, which has one as the value of the form. Then the test simulates a click on the “minus button”. After that, the value of the form is checked; it should be one (not zero), or else the test will fail.
After writing the above test, I can commit the code with the [RED] tag, as the tests have finished being written. When the pipeline finishes, I look at the results. The results show that the tests have failed. With that, I can now implement the code that will make the tests return green.
import React from 'react';
import Button from 'react-bootstrap/Button';
import InputGroup from 'react-bootstrap/InputGroup';
import FormControl from 'react-bootstrap/FormControl';
import './JumlahSesiPerHariComponent.css';
class JumlahSesiPerHariComponent extends React.Component {
constructor(props) {
super(props);
this.state = { jumlahSesi: 1 }
}
setJumlahSesi(jumlahSesi){
if (jumlahSesi < 1){
this.setState({jumlahSesi: 1})
}
else{
this.setState({jumlahSesi: (jumlahSesi)})
}
}
getJumlahSesi(){
return this.state.jumlahSesi;
}
render() {
return <>
<InputGroup id = "jumlahSesiInputGroup">
<h5 id="jumlahSesiText">Jumlah sesi per hari: </h5>
<div className="jumlahSesiInputContainer">
<Button id="minusButton" onClick={() => this.setJumlahSesi(this.state.jumlahSesi - 1)}>-</Button>
<span className="buttonPadding"></span>
<FormControl id = "jumlahSesiForm" disabled value ={this.state.jumlahSesi}/>
<span className="buttonPadding"></span>
<Button id="plusButton" onClick={() => this.setJumlahSesi(this.state.jumlahSesi + 1)}>+</Button>
</div>
</InputGroup>
</>
}
}
export default JumlahSesiPerHariComponent;
After the above code is written, the tests that I wrote returned green. I can now commit the code with the [GREEN] tag. Below is how the component looks after the code above is written.

At this step I can now check whether or not the code needs refactoring. The code has no duplicate components, and the code is already structured logically. It is simple and readable, too. Therefore I deem that this code does not need refactoring, and I declare that this feature is finished. I can now move on to the other features that needs implementing.
What I learned from Test Driven Development
Test Driven Development is not a new concept to me as I have used it for around two and a half years throughout my university life. But there are a few notable things that I would like to highlight on what I have found, learned and implemented in this project.
Test Driven Development Requires You to Learn How to Implement the Actual Product Before Implementing it.
Write-as-you-go does not work when you are using this method of development. You must study the implementation first to be able to test it. Our application consists of multiple components that are implemented separately. Therefore there must be a test that tests the component separately, and if the component is combined, there must be a test that tests the component when it is combined.
Test Driven Development Requires You to Think About All the Possible Inputs and Outputs.
One thing that I like the most about implementing Test Driven Development is that every single possibility of input and output must be accounted for before the code is written. To write a test that accounts for every single possibility of input, we need to determine first what is going to be inputted into the data. For file inputs in our software, we are provided a sample file for input. Other inputs for options are integers and dates. This becomes our standard input that we have to test on.
This may take a longer time to do. But we had a very creative idea that I implemented to solve this problem. One of the solution so that the inputs are easier to write tests on is to limit the inputs that the users can implement so that the users cannot submit what we consider wrong input. For instance, we had a date input component that users can only input the numbers in the range 1-31. The component that we showed above also has buttons as the only input methods that the users can use.
Other than limiting the inputs, we also have input checkers to ensure that what the users input is acceptable. One case where input checkers are implemented is on the files. It has to be in excel format otherwise it won’t be accepted. This helps us so that we need to only consider the cases where an excel file is inserted into the file input.
Test Driven Development Ensures that Your Code Will be Tested for Quality Every Time it is Pushed to the Shared Repository and There is a Reason why such Automation is Important.
As the person responsible for DevOps in my team, along with front end development, I have the responsibility of configuring CI/CD for the app that we’re working on. Our CI/CD code ensures that whenever we pushed a code to the repository, the code is tested first before being deployed.
On the Yaml file that I have created I wrote multiple stages. Here is the snippet that shows the stages that I have created.
stages:
- test
- linter
- qa
- staging-frontend
- staging-backend
test-frontend:
image: node:slim
stage: test
before_script:
- cd frontend
- yarn install
script:
- npm run test --coverage
artifacts:
paths:
- ./frontend/coverage
expire_in: 1 hrs
tags:
- docker
test-backend:
image: node:slim
stage: test
before_script:
- cd backend
- npm install
script:
- npm run test --coverage
artifacts:
paths:
- ./backend/coverage
expire_in: 1 hrs
tags:
- docker
linter-frontend:
image: node:slim
stage: linter
before_script:
- cd frontend
- npm install
- npm install eslint@6.x --save-dev
script:
- npx eslint -f json -o report.json src/
artifacts:
paths:
- ./frontend/report.json
expire_in: 1 hrs
tags:
- docker
linter-backend:
image: node:slim
stage: linter
before_script:
- cd backend
- npm install
- npm install eslint --save-dev
script:
- npx eslint -f json -o report.json ./
artifacts:
paths:
- ./backend/report.json
expire_in: 1 hrs
tags:
- docker
SonarScanner:
image: addianto/sonar-scanner-cli:latest
dependencies:
- test-frontend
- test-backend
- linter-frontend
- linter-backend
stage: qa
script:
- sonar-scanner -X -Dsonar.host.url="https://pmpl.cs.ui.ac.id/sonarqube" -Dsonar.login=$SONARQUBE_TOKEN -Dsonar.branch.name=$CI_COMMIT_REF_NAME -Dsonar.projectKey=$SONARQUBE_PROJECT_KEY
staging-frontend:
image: ruby:2.4
stage: staging-frontend
before_script:
- cd frontend
- gem install dpl
- wget -qO- https://cli-assets.heroku.com/install-ubuntu.sh | sh
script:
- dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_API
- heroku run --app $HEROKU_APP_NAME migrate
environment:
name: production
url: $HEROKU_APP_HOST
only:
- staging
staging-backend:
image: ruby:2.4
stage: staging-backend
before_script:
- cd backend
- gem install dpl
- wget -qO- https://cli-assets.heroku.com/install-ubuntu.sh | sh
script:
- dpl --provider=heroku --app=$HEROKU_APP_NAME_BACKEND --api-key=$HEROKU_API
- heroku run --app $HEROKU_APP_NAME migrate
environment:
name: production
url: $HEROKU_APP_HOST
only:
- staging
Notice that there exists a test stage in the yaml file. This stage contains code that will run the tests that were made for the web application (npm run test –coverage). The results of the test will be exported to a json file and exported as an artifact that will be sent to SonarQube, our Code Quality Checker, only when the test succeeds. When the tests have failed, the Gitlab Pipeline will simply return a red X.
In addition to the test stage, we also have a linter stage that will test the cleanliness of the code. The results will also be exported as a json artifact and sent to SonarQube. If at any point the linter fails, the Gitlab Pipeline will simply return a red X.
Now you might be wondering, why do you need SonarQube then if when the test fails it will be shown on the Gitlab Pipeline?
The answer to that question is because SonarQube has a completely different job. It doesn’t test the code and it doesn’t lint the code, it is a reporting tool which reports on duplicated code, coding standards, unit tests, code coverage, code complexity, comments, bugs, and security vulnerabilities. This allows us to not worry about missing the things that we forget to check and the mistakes that we might have missed as it reports and informs it to us automatically, which helps quicken the code review process and lowers the maintenance time. If the code has any issues, then the quality gate will return red which indicates that the code has failed the Quality Test. Otherwise, the code will return green.
Test Driven Development Requires You to Check Not Just How the Application Code is Structured, But How the Pipeline for the CI/CD is Written and Run.
There may be numerous times when your code passes the test in local, and for some reason does not pass the test in CI/CD. Perhaps your global environment has the necessary components installed whereas on CI/CD you don’t even have the commands to install those components. Perhaps the runner abruptly stopped. Perhaps your friend has created a [RED] commit for some other component and you’re trying to pass yours. This results in [GREEN] Commits being marked red by Gitlab and it looks bad. These are some of the things that happened to me.
To fix this, one must modify the code pertaining to the CI/CD process, such as the code that is executed when the web application has finished deploying to the server, the code that is executed when it is needed to be built, and the specific package manager that is used so that the code that runs the tests and the build process will actually execute without problems (e.g. in my project, we use yarn instead of npm and because of that we had to change the template code so that it executes yarn). We had to also add a Procfile since we deployed our front end to Heroku.
Since the target of deployment for staging is Heroku, we also need to check the logs to determine whether or not the deployment process succeeds. We can do this through the Heroku dashboard, or through the command line. I prefer doing it through the Heroku dashboard.
Test Driven Development Helps Provide Clarity Not Just to Me, but My Teammates as Well.
Having an idea about all of the things that I need to implement helps in development as I can easily implement them in a in a relatively short period of time. (at least for front-end components with a very simple design that’s the case). The test really provides clarity to me as it explains by itself what I should implement and the results that I should obtain after implementing the code.
However, I am not the only one that benefits from this. In the process of checking the code, my teammates can verify that the code is working perfectly and is written according to the inputs and outputs specified on the test cases. This quickens the code review process. Plus, after the refactor process, the code should be reasonably clean, unless there is something that I have missed.
The Correct Way to Implement Tests is not to Arbitrarily Make the Coverage 100%, but to Create Meaningful Tests that Tests the Functionalities of the App Extensively.
Even if the test code coverage is 100% on SonarQube, our Code Quality Checker, we must also ensure that the test covers all inputs and outputs. Good tests always test every possible form of input that users can use, and whether or not the expected output is given by the program, regardless of whether or not the same line of code must be covered multiple times, or just once.
Conclusion
While Test Driven Development is not easy to use at the beginning, you may soon realize that you will not be able to write code without it due to the multitude of benefits that it has. It keeps the code that you write to a minimum, as the code that you wrote will conform to the tests that have been previously written. You can also be sure that if the code passes all correctly written tests, the code will be of a reasonably high quality. The code and the tests that you have written help expedite the code review process. It is literally the way to go if you want to create high quality code while shortening the maintenance time at the same time. So, try using it in your next project. You might be surprised at the benefits that it gives you.
Sources
University Lectures, Documents and Modules