Intro to Unit Tests

Abhinav
interleap
Published in
12 min readJan 14, 2022

A beginner level introduction covering why automated unit testing, what is it exactly, and how to get started.

When I passed out of college and started my first job at Synygy back in 2006, there were 3 techniques used by Synygy that blew my mind — Version Control, Automated build systems (they were using CI/CD in 2006 without calling it that), and Unit Tests.

Looking back, they were ahead of their time. Today, most companies and college students have adopted Version Control. But as far as CI/CD and Unit Tests go, there is still a gap.

This is a long blog, that aims to give a comprehensive introduction to Unit Tests, including what they are, their history, why write them, and how to write them.

Why Do You Write Tests?

Let’s take a simple programming example to understand this -

Whatsapp Last Seen Formatter: Return the Whatsapp ‘last seen’ time in a human readable format, given the exact number of seconds before which the person was online.Example: 
If the person was online less than a second ago, it should return “Online”
If the person was last online 59 seconds ago, it should return “Last seen 59 seconds ago”
If the person was last online 60–119 seconds ago, it should return “Last seen 1 minute ago”
If the person was last online 60 * 60 seconds ago, it should return “Last seen 1 hour ago”
…and so on for days and weeks

I can use Kotlin’s amazing pattern matching syntax to come up with a quick function -

fun lastSeen(duration: Int): String {
return when (duration) {
0 -> "Online"
in 1 until 60 -> "Last seen $duration seconds ago"
in 1 * 60 until 60 * 60 -> "Last seen ${duration / 60} minutes ago"
in 60 * 60 until 60 * 60 * 24 -> "Last seen ${duration /(60 * 60)} hours ago"
else -> "Last seen ${duration /(60 * 60 * 24)} days ago"
}
}

But the question is, how do I test it? Can I be sure that my code works properly for all scenarios? To make sure, I’ll have to run the code for a few different random inputs, such as 0, 1, 4, 59, 60, 61, 120, 60 * 60 etc.

If I’m on the Whatsapp Android team, and writing this code for them, the task of testing becomes significantly more difficult. I’ll have to use 2 phones, come online on 1, and check the status on the other after 1 second, 2 seconds, a few hours, etc etc.

What happens if I find a bug after a few hours? I’ll fix the bug, and then again repeat the tests which will again take a few hours / days / weeks for me to finish.

This flowchart gives us an idea of where development time goes in such cases -

Each tiny change forces the developer to spend hours in testing, resulting in days of wasted time and frustration

This sounds quite painful! Can we do something that’s easier?

Testing real life complex applications becomes quite difficult after a while, and the solution to this problem is automated tests. We can write a program that tests our program by providing different inputs and outputs, and telling us if it fails for any of these scenarios.

This way, every time we make a modification to the lastSeen function, we can run our test program, and instead of testing taking hours / days, it would now take us just milliseconds.

Here is the updated flowchart of where the development time goes -

The reduction in testing time results in less time waste and frustration

Example

Here is how the test looks for the example we gave above -

internal class LastSeenTest {
@Test
fun showOnlineStatusForLastSeenDurationOfZeroSeconds() {
assertEquals("Online", lastSeen(0))
}

@Test
fun showLastSeenStatusForOneSecond(){
assertEquals("Last seen 1 second ago", lastSeen(1))
}

@Test
fun showLastSeenStatusForMultipleSeconds(){
assertEquals("Last seen 59 seconds ago", lastSeen(59))
}

@Test
fun showLastSeenStatusForOneMinute(){
assertEquals("Last seen 1 minute ago", lastSeen(60))
}

@Test
fun lastSeenShouldRoundDownTheNumberOfMinutes(){
assertEquals("Last seen 1 minute ago", lastSeen(119))
}

@Test
fun showMinutesInPluralForLastSeenMultipleMinutesAgo(){
assertEquals("Last seen 50 minutes ago", lastSeen(50 * 60 + 10))
}
}

It took me around 5 minutes to write these tests, and a couple of seconds to run and see the result. I was immediately able to find a few bugs in my code. The bug that I found is that singular units are not handled — for example, for one second, it says “1 seconds ago” and not “1 second ago”. Same for minutes, hours etc.

But now I have tests! So every time I fix a bug or make another change, I can run the tests in a few seconds and verify that things work, instead of putting in hours of tedious and boring manual effort.

Benefits of Writing Tests

While Unit Testing was initially invented to check whether the code works fine or not, over time we’ve realised that unit tests provide many more benefits that were not initially apparent —

  1. Clean Interface: A test is a consumer of the code and its API. Thus, the process of writing a test gives immediate feedback about how easy it is to use the function / class / module. As a result, modules that have tests end up having simpler, cleaner contracts.
  2. Working Code: Developers develop features incrementally, and can test them as soon as they write a part of code.
  3. Refactoring Friendly Environment: Having a comprehensive test coverage allows developers to be brave and refactor without having the stress of breaking the code at multiple places every time they refactor.
  4. Documentation: A well written test suite acts as living documentation of what the code does.
  5. Less Bugs: Sometimes, a small change in code can cause a totally unexpected side effect. Having comprehensive tests allows the team to catch these bugs during the development phase, and not them slip.
  6. Faster Time To Market: Teams wait sometimes days and sometimes weeks before releasing software, even if they’ve finished development. This is because changes bring bugs and the teams don’t remain confident that they’ve created a working, bug-free software. Then teams spend weeks testing and verifying that everything works. A good quality test suite ensures that the software is always working, and can go live much faster.
  7. Developer Happiness: Testing the entire code again and again after making a small change becomes tedious and frustrating. This leads to developers trying to avoid any major changes or improvements in the software, just to avoid the stress of unexpected bugs and testing effort. Unit Tests provide the confidence and security to the developer to work without having to deal with this stress.

Summary: Unit tests allow developers to develop faster by reducing the burden of testing. They also lead to developer happiness by reducing the tediousness of testing the same thing again and again. They also provide additional benefits that are not very obvious, but become apparent as the team writes more and more tests.

History of Unit Tests

Automated tests can be at any level: starting from unit tests that test individual functions, to E2E tests that actually open the software and perform actions.

However automated testing techniques were not formalised in the ’80s and the ’90s. People had their own ways and frameworks to write tests. However these frameworks were often custom, and not easy to use.

In ‘94, a developer named Kent Beck was on his way to work with a client in Chicago, and wanted to advise them to write automated tests, but did not have a mechanism in Smalltalk ready which he could ask them to use.

To make the process developer friendly, he created a simple and easy to write framework with around 3 classes and 12 methods that allowed developers to write tests that could verify the return values of individual methods. He called this framework SUnit.

According to him, it was so small and simple, that he really did not think much about it. He sent it to another developer he know who was working on something very complex and low level. After around 6 weeks, the developer wrote back explaining how useful it was for him in making the complex system work, and that is when Kent realised how good this was.

Post this, unit testing really took off, because it allowed developers to verify small parts of their code as they worked on it. This inspired the whole xUnit family of frameworks including JUnit.

Here is Kent Beck describing Unit Testing. The first 10 minutes or so explain the part about inventing JUnit.

Composition of a Unit Test

Unit Testing in software is based on the concept of Test Fixtures. Test Fixture is a generic term also used in electronics, that defines an environment that is created specifically to test one component under a certain set of conditions.

But rather than explain the composition of a unit test with more technical jargon, let’s start at the very basics.

Let’s use a Unit Test from the previous example to demonstrate the different parts that make up the unit test in software. I’ve extracted a couple of variables to make the test easier to explain.

@Test
fun showLastSeenStatusForMultipleSeconds(){
val expected = "Last seen 59 seconds ago"
val
actual = lastSeen(59)
assertEquals(expected, actual)
}

There are atleast 2 distinct phases in the test above.

  1. lastSeen(59) is called the Exercise phase. Here, the test performs the operation it wants to test with the exact input, and records the result.
  2. assertEquals(expected, actual) is called the Verify phase. Here, the test verifies that the actual result produced by the software matches the expectation.

This teaches us about 2 phases of software testing — Exercise and Verify.

Let’s look at another unit test to understand the 2 other phases. This unit test verifies that the course service is able to find a course with the expected name.

@Test
public void findCourseByCourseName(){
//Set Up
savedCourse = courseRepository.save(new Course(null, "course1", "subtitle1", null, true, 400.00));

//Exercise
final var retrievedCourse = courseService.findByName("course1");

//Verify
assertEquals(savedCourse, retrievedCourse);

//Tear Down
courseRepository.deleteAll();
}

In this example, we require 4 phases. I’ve commented above each phase to explain what it’s doing.

  1. Setup (Arrangement): Creates the right environment in which we want to test our software. For example, a banking application might want to test what happens if there’s no money in the account, but the customer tries to withdraw money. In that case, the setup phase will create an account with no money, and the exercise phase will perform the action of withdrawing money. In the above example, the only setup required is saving the object via the repository.
  2. Exercise (Action): The subject under test (a method / class) performs the action post setup with a particular set of inputs, and produces a result. In the example above, the exercise step finds the course using its name.
  3. Verify (Assertion): Verifies that the output produced by the action given the setup and action is correct.
  4. Tear Down: Resets the software to the initial state. Includes closing any open connections, deleting any records put into the database as a result of the setup or action, freeing up any memory. In the banking example, this would mean removing the fake account inserted into the database.

There is another term for the phases above — AAA or Arrange, Act, Assert. This terminology makes it easier to remember the phases above, but forgets to include the tear-down phases which is equally important.

Write your first Unit Test with JUnit

This section will show you how to setup a Gradle project with JUnit and Java / Kotlin, and write a unit test.

If you get confused at any step, you can open the JUnit 5 Starter Guide’s example projects section, or Gradle’s Junit5 section to help you out. They provide the same information in a lot more detail.

Step 1: As a first step, create a Gradle project with either Java or Kotlin. I highly recommend using an IDE like IntelliJ Idea or Eclipse to help you out with this.

This section below assumes that you have a Java / Kotlin project setup with Gradle.

Step 2: To add JUnit, add the following lines to the dependencies in build.gradle

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
testImplementation "org.mockito:mockito-core:3.+"

Step 3: Next, add the following to your build.gradle

test {
useJUnitPlatform()
}

Here’s a snapshot of my build.gradle after these steps. You can ignore the Kotlin specific dependencies if you’re using Java.

plugins {
id 'org.jetbrains.kotlin.jvm' version '1.5.10'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.6.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
testImplementation "org.mockito:mockito-core:3.+"
}

test {
useJUnitPlatform()
}

Step 4: Add a package called org.example under src/main/kotlin or src/main/java

Similarly, create the same package under src/test/kotlin or src/test/java

Step 5 (Java): Under src/main/java, create a class called LastSeen , and under src/test/java create a class called LastSeenTest .

Step 5 (Kotlin): Under src/main/kotlin , create an empty file called LastSeen.kt, and under src/test/kotlin , create a class called LastSeenTest.

Step 6 (Java): Put the following contents under theLastSeen and LastSeenTest classes -

LastSeen class -

package org.example;

public class LastSeen {
public String lastSeen(int duration){
if(duration == 0) return "Online";
else return "Not implemented yet";
}
}

LastSeenTest class -

package org.example;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class LastSeenTest {
@Test
public void showOnlineStatusForLastSeenDurationOfZeroSeconds(){
assertEquals("Online", new LastSeen().lastSeen(0));
}
}

Step 6 (Kotlin): Put the following contents under the file LastSeen.kt and the class LastSeenTest

LastSeen -

package org.example

fun lastSeen(duration: Int): String {
return when (duration) {
0 -> "Online"
else
-> "Not Implemented Yet"
}
}

LastSeenTest -

package org.example

import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test

internal class LastSeenTest {
@Test
fun showOnlineStatusForLastSeenDurationOfZeroSeconds() {
assertEquals("Online", lastSeen(0))
}
}

Step 7: In your IDE, you will now see a green play button against your test class. Click that to run the test!

Here’s how the button looks on IntelliJ -

Solve the Entire Problem

Congratulations! You have just written your first Unit Test!

Once you get your test up and running, I strongly recommend that you practice by completing this example step by step.

First, choose the scenario you want to implement, and write the test yourself. For example, you might write the test to correctly display ‘Last seen 1 second ago’.

Then, make your code work for this particular scenario by implementing the logic.

Keep doing this iteratively, and you would ensure that every single line of code you’ve written is backed by a test that ensures that if you ever introduce a bug, it will get caught!

Next Steps

If you’ve reached here, you now know a bit about Unit Tests. At this point, I want to give you a few suggestions on how you can take your learning of Unit Tests even further.

Practice. With the knowledge of Unit Testing fresh in your mind, start practicing writing tests, especially for simple programming contest type questions. They help you build the muscle memory on how to write tests.

Setup dummy projects with Unit Tests. Pick a standard framework that is often used in your personal projects or day job, such as Spring, Node, Django, Android. For standard web and mobile frameworks such as Spring, Django, Rails, Node, React, Android, iOS, there are popular existing solutions available, and the framework often has instructions on how to set up Unit Tests for you. Then practice writing a few tests for a few dummy features. This will help you learn how Unit Testing can be applied and used in real life.

Learn about Mocks and when to use them. You can test interactions with databases etc by setting up test databases, or by using mocks. Which approach to use depends on the scenario. But by learning about Mocking, you get the power to choose which approach you want to go with.

Start writing Unit Tests on your day job: This is the last and final step, and the most difficult of the lot. I give more suggestions below on how to approach this.

Writing Unit Tests On Your Day Job

Start by setting up a Unit Testing framework in your project. For standard web and mobile frameworks such as Spring, Django, Rails, Node, React, Android, iOS, there are popular existing solutions available, and the framework often has instructions on how to set it up for you.

Write tests for the simplest of functions. Find tiny pieces of logic in your code for which you can write tests. Something like a for loop, or a function with a a couple of conditions. This helps you kick-start the process of writing tests.

Break Code into Independent Functions and Write Tests. Often, it will be nearly impossible to test large complex functions. This problem can be simplified by breaking these functions into smaller ones, extracting independent files or classes, and writing tests for individual pieces of logic.

Learn about Mocks and when to use them. You can test interactions with databases etc by setting up test databases, or using mocks. Which approach to use depends on the scenario. But by learning about Mocking, you get the power to choose which approach you want to go with. We have an amazing course on Udemy that teaches exactly this.

If you liked this article, please share this on social media, and subscribe to us. We share our work and thought process in the form of online courses and blogs. You can check us out here -

Website: https://interleap.co
Blog: https://medium.com/interleap
LinkedIn: https://www.linkedin.com/company/interleap
Udemy: https://www.udemy.com/user/abhinav-manchanda/ and https://www.udemy.com/user/omkar-birade/

--

--