>  Blog

Test-Driven Development: An Introduction with a JUnit Example


Pradyumn Sharma

April 11, 2017


One of the most important practices from Extreme Programming, which I cannot overemphasize, is Test Driven Development (TDD). This is a great way of achieving clarity about the requirements from a system / component / story / class, as well as verifying that the implementation meets our requirements.

In this article, I'll talk about test-driven development at the level of a single class. Testing at this level is called unit testing, as it tests a single unit of source code. The same concepts can also be applied on a larger scale, involving an entire story, or a module, end-to-end workflow, or even an entire system. But I'll not cover those aspects in this article.

Example: Binary Search Algorithm

Let us take an example. Suppose I want to implement the binary search algorithm. Remember binary search? Most of us learnt that in college.

Binary search is one of the techniques for searching for a value (say, an integer) inside a sorted array. A simpler, but less efficient approach is linear search, in which compare every element of the array with the search value, until we find the value or we are able to conclude that it is not present in the array.

In binary search, we first look at the middle position in the array. If we are very lucky then the search value is found right there. But even otherwise, either the value we are looking for is LESS THAN the value in the middle of the array, or it is GREATER THAN that. So in both these cases one half of the array is ruled out and we have a problem that is, in one single step, half the size of the original problem.

If you know the Big-O notation, the linear search approach is O (n), while the binary search is O (log2n), a significant improvement in efficiency.


Typical, Non-TDD Approach

Most of the times, the approach that we see in practice is somewhat like this:

  • Developer writes code.
  • He believes that the code is complete. Now time for some testing.
  • He writes some more code to accept inputs for testing at the console and display the result.
  • He runs the test:
    • Enter array values: 11, 22, 33, 44, 55
    • Enter value to search for: 22
    • Result displayed: 1 (arrays having zero-based indexes)
    • Conclusion: output is correct
  • Enter some more data for a failing scenario, such as:
    • Enter array values: 11, 22, 33, 44, 55
    • Enter value to search for: 20
    • Result displayed: -1
    • Conclusion: output is correct
  • End of developer testing. Release the code to QA.
  • QA finds a defect.
  • Developer fixes the defect. Runs a test for the defect reported. The test passes. May re-run earlier tests.
  • QA finds another defect.
  • Developer fixes this defect also. Runs a test for the defect reported. The test passes. Does not re-run earlier tests, as it getting a little too bothersome to enter the same data again and again.
  • The cycle of defect-fix-test continues for many more rounds.

The TDD Approach: An Overview

The TDD approach upends the cycle of code-test-defect-fix. In this approach, I would begin by identifying various scenarios for which my class implementing the binary search algorithm will need to be tested:

  • An array with an odd number of elements (such as 7), and searching for
    • The value at the middle position
    • The first value
    • The last value
    • Some other values in between
    • A missing value less than the first value
    • A missing value greater than the last value
    • Some other missing value
  • An array with an even number of elements (such as 8), because in such a case the middle position is ambiguous. Similar test condition as above, except that I should look for both the middle positions
  • Should I explicitly test for an array having only one element? Even though, one is an odd number, but no harm in testing for this special case. Sometimes, our programs do fail for boundary conditions, even if those are simple cases.
  • I would then prepare my test data, in order to test the various scenarios that I had identified earlier:

    Array contents

    Search value

    Expected result

    11, 22, 33, 44, 55, 66, 77

    11

    0

    44

    3

    77

    6

    33

    2

    9

    -1

    45

    -1

    100

    -1

    11, 22, 33, 44, 55, 66, 77, 88

    11

    0

    44

    3

    55

    4

    77

    6

    88

    7

    33

    2

    9

    -1

    45

    -1

    100

    -1

    11

    11

    0

    9

    -1

    45

    -1


    To my perhaps less-experienced mind, these test cases suffice. It is possible that I'll think of some other conditions later, for which my class needs to be tested. Or a tester with a sharper mind will identify some such cases. But for the time being, the above test cases seem adequate to me.

    Automating Tests with JUnit

    Time now to automate my test infrastructure. Even before I write the first line of actual code for binary search algorithm.

    I am going to develop my class in Java, so I'll use JUnit, an open-source framework for automating unit tests. There are similar unit testing frameworks for other programming languages (such as MSTest for .NET, CppUnit for C++, etc), but more about those some other time.

    I am going to use Eclipse as the IDE, which comes bundled with JUnit. But all other prominent IDEs for Java (such as NetBeans and JDeveloper) also support JUnit with equal ease.

    I create a new project in Eclipse. I am going to name my class (that implements the binary search algorithm) as, well, BinarySearch. But I am not going to create this class now. I am first going to create the class that will test BinarySearch.

    Creating a Test Class

    I decide that the class for implementing the binary search will be named BinarySearch (but I'll implement it later). But first, I should have a class that will test the BinarySearch class. Following the common convention for naming JUnit test classes, I'll call the test class BinarySearchTest. So I begin my Eclipse project by creating a new "JUnit Test Case" in an appropriate package:



    I specify the name for the test class:



    Eclipse creates the class with the default code as follows:

    public class BinarySearchTest {
    	@Test
    	public void test() {
    		fail("Not yet implemented");
    	}
    }

    A method with the @Test annotation is, well, a JUnit test method. Test methods are, by convention, named using the scheme test[thisbehavior], as we'll see in a listing in a minute.

    Creating the Test Methods

    So I replace the default test method created by Eclipse with the following methods, all empty initially:

    public void testArrayWithOddNumberOfElements() {
    }
    public void testArrayWithEvenNumberOfElements() {
    }
    public void testArrayWithOnlyOneElement() {
    }
    

    I start by implementing only one test case in the testArrayWithOddNumberOfElements() method, as follows:

    public void testArrayWithOddNumberOfElements() {
    	int [] collection = new int [] 
    				{11, 22, 33, 44, 55, 66, 77};
    	assertEquals (0, BinarySearch.search(collection, 11));
    }
    

    assertEquals is one of the methods provided by JUnit, to compare the expected and the obtained results. Its syntax is:

    assertEquals (<expectedValue>, <actualValue>)

    where <actualValue> is replaced by a call to the method that we want to test.

    As Eclipse complains about the missing class BinarySearch, I ask it to create one for me. Then, with some help from Eclipse, I create the method int search (int [] collection, int value) in the BinarySearch class.

    Starting with a Failing Implementation

    I must have some implementation in the search method that returns an int. I'll worry about the actual implementation later, but for the time being, I write a failing implementation (an implementation that should always fail) as follows:

    public class BinarySearch {
    	public static int search(int[] collection, int value) {
    		return -2;
    	}
    }

    With this failing implementation and a single test case, I run the test



    JUnit displays a red bar. That means test failed. That was expected, of course.



    I have taken the first baby steps for building my test infrastructure. I can start some serious work now.

    Implementing the Test Methods

    I now implement all the test methods in the BinarySearchTest class, with the test data that I had already identified:

    public class BinarySearchTest {
    	@Test
    	public void testArrayWithOddNumberOfElements() {
    		int [] collection = new int [] 
    				{11, 22, 33, 44, 55, 66, 77};
    		assertEquals (0, BinarySearch.search(collection, 11));
    		assertEquals (3, BinarySearch.search(collection, 44));
    		assertEquals (6, BinarySearch.search(collection, 77));
    		assertEquals (2, BinarySearch.search(collection, 33));
    		assertEquals (-1, BinarySearch.search(collection, 9));
    		assertEquals (-1, BinarySearch.search(collection, 45));
    		assertEquals (-1, BinarySearch.search(collection, 100));
    	}
    
    	@Test
    	public void testArrayWithEvenNumberOfElements() {
    		int [] collection = new int [] 
    				{11, 22, 33, 44, 55, 66, 77, 88};
    		assertEquals (0, BinarySearch.search(collection, 11));
    		assertEquals (3, BinarySearch.search(collection, 44));
    		assertEquals (4, BinarySearch.search(collection, 55));
    		assertEquals (6, BinarySearch.search(collection, 77));
    		assertEquals (7, BinarySearch.search(collection, 88));
    		assertEquals (2, BinarySearch.search(collection, 33));
    		assertEquals (-1, BinarySearch.search(collection, 9));
    		assertEquals (-1, BinarySearch.search(collection, 45));
    		assertEquals (-1, BinarySearch.search(collection, 100));
    	}
    
    	@Test
    	public void testArrayWithOnlyOneElement () {
    		int [] collection = new int [] {11};
    		assertEquals (0, BinarySearch.search(collection, 11));
    		assertEquals (-1, BinarySearch.search(collection, 9));
    		assertEquals (-1, BinarySearch.search(collection, 45));
    	}
    }
    

    Just to be sure, I run the tests.



    The tests fail. All of them. Notice the small cross mark against each test method name. If the tests had not failed, I would have been shocked.

    Implementing the BinarySearch Class

    Now, I have the complete test infrastructure in place. I can finally turn my attention to implementing the BinarySearch class. I write some code. Run the tests. Some tests pass, some fail. I modify the code. Again I run all the tests. I repeat this process till all tests pass.

    Here is the code that manages to get all the tests pass:

    public class BinarySearch {
    
    	public static int search(int[] collection, int value) {
    
    		int size = collection.length;
    		int lower = 0;
    		int upper = size - 1;
    		int middle;
    		
    		while (lower <= upper) {
    			middle = (lower + upper) / 2;
    			if (collection [middle] > value) {
    				upper = middle - 1;
    			} else if (collection [middle] < value) {
    				lower = middle + 1;
    			} else // we found the data
    				return middle;
    		}		
    		return -1;
    	}
    
    }
    

    This is how JUnit informs me that all the tests have passed, with a green bar.



    Now I am confident of my BinarySearch class. I request a tester to help me with my test cases, and see if she can identify any other scenarios that I have missed out. If we find some such scenarios, I add more test methods or assertions to the test class.

    Trying Out Some Alternative Implementation

    I look at my code in the BinarySearch class again. It is late in the day. My mind is a little foggy. I am not sure, but I feel that perhaps the statement

    upper = middle - 1;

    ought to be

    upper = middle;

    And similarly, the statement

    lower = middle + 1;

    ought to be

    lower = middle;

    I can try these changes and see whether the program continues to work as expected. Here is the modified code:

    public class BinarySearch {
    
    	public static int search(int[] collection, int value) {
    
    		int size = collection.length;
    		int lower = 0;
    		int upper = size - 1;
    		int middle;
    		
    		while (lower <= upper) {
    			middle = (lower + upper) / 2;
    			if (collection [middle] > value) {
    				upper = middle;
    			} else if (collection [middle] < value) {
    				lower = middle;
    			} else // we found the data
    				return middle;
    		}		
    		return -1;
    	}
    
    }
    

    I run the code, hoping to see a green bar. Or fearing a red bar. But nothing appears. The program seems to have gone into an infinite loop. I manually terminate the program.

    Timeout as Test Failure

    When my program goes into an infinite loop, there ought to be a better way than my waiting for a long time and then terminating it. There is. I can ask JUNit to report a test as having failed if it takes longer a specified amount of time.

    For example, given the amount of data in my test methods, five seconds can be a very generous upper limit of time for these to complete. I can specify this time limit with a timeout parameter (in milliseconds) as shown in the code below:

    public class BinarySearchTest {
    
    	@Test (timeout=5000)
    	public void testArrayWithOddNumberOfElements() {
    		int [] collection = new int [] 
    				{11, 22, 33, 44, 55, 66, 77};
    		assertEquals (0, BinarySearch.search(collection, 11));
    		assertEquals (3, BinarySearch.search(collection, 44));
    		assertEquals (6, BinarySearch.search(collection, 77));
    		assertEquals (2, BinarySearch.search(collection, 33));
    		assertEquals (-1, BinarySearch.search(collection, 9));
    		assertEquals (-1, BinarySearch.search(collection, 45));
    		assertEquals (-1, BinarySearch.search(collection, 100));
    	}
    
    	@Test (timeout = 5000)
    	public void testArrayWithEvenNumberOfElements() {
    		int [] collection = new int [] 
    				{11, 22, 33, 44, 55, 66, 77, 88};
    		assertEquals (0, BinarySearch.search(collection, 11));
    		assertEquals (3, BinarySearch.search(collection, 44));
    		assertEquals (4, BinarySearch.search(collection, 55));
    		assertEquals (6, BinarySearch.search(collection, 77));
    		assertEquals (7, BinarySearch.search(collection, 88));
    		assertEquals (2, BinarySearch.search(collection, 33));
    		assertEquals (-1, BinarySearch.search(collection, 9));
    		assertEquals (-1, BinarySearch.search(collection, 45));
    		assertEquals (-1, BinarySearch.search(collection, 100));
    	}
    
    	@Test (timeout = 5000)
    	public void testArrayWithOnlyOneElement () {
    		int [] collection = new int [] {11};
    		assertEquals (0, BinarySearch.search(collection, 11));
    		assertEquals (-1, BinarySearch.search(collection, 9));
    		assertEquals (-1, BinarySearch.search(collection, 45));
    	}
    }
    

    I run my tests again. This time JUnit reports all the test methods as having failed because of timeout.



    I think that's cool.

    Back to Safety

    As my experiment with the modification failed, I revert the code to the earlier version, and run the tests again. This time, as expected, all the tests pass again.

    Final Words

    This is an introductory tutorial to the concepts of TDD, as well as the basics of JUnit. JUnit has many many more features to help in specifying the expectations from our code and verifying that these expectations continue to be met. For more details on JUnit, you may visit www.junit.org.