JUnit Testing in Java

JUnit Testing

Welcome back to another Java tutorial. Last time we learned about control flow and revisited the basics of logic. Now, we’re going to learn how to test our code using a framework known as JUnit testing.

Table of Contents

Debugging

As logic gets more complicated, we’re going to start to notice more program errors called bugs. In fact, we have already noticed this in the previous lesson while playing with if statements.

As it turns out, the process of fixing errors is known as debugging, and it’s an incredibly important skill to have. Fortunately, Java comes stock full of tools to help us identify and fix bugs. But before we get to that, let’s try to look at a few bug examples.

Bad Branch Example

Remember previously when we introduced if statements? In that lesson, we talked about a topic known as branching. Branching increases complexity of a program by increasing the paths that a program can take. As branches increase, the possibility for bugs to develop increases.

Now, the challenge is to be sure all of these branches are bug free. However, if a bad branch only ever executes 1 in million times, it may be awhile before we ever notice. That’s assuming that we aren’t already checking for that case.

If we’re not checking our branches ahead of time, then we’ll inevitably run into issues down the line. That’s where debugging comes in. Debugging is the process of tracking down a bug and resolving it. Let’s start with an example.

public static boolean isPositive(int num) {
    // Assume false
    boolean state = false;
 
    if (num > 0) 
        System.out.println("num is positive");
        state = true;

    return state;
}

The sample above is innocent enough. To test it, we’ll try compiling it and running it using Dr. Java’s interactions pane.

Here we’re assuming isPositive() is inside some class. Let’s call it MyMathWorkshop. That way we can easily pass values to this method by calling something along the lines of MyMathWorkshop.isPositive(num). As long as we pass positive numbers to it, we’re happy.

However, eventually we’ll hit the case where we pass a negative number as input, and the method will return true:

MyMathWorkshop.isPositive(2);    // Correctly returns true
MyMathWorkshop.isPositive(-7);   // Incorrectly returns true

Debugging with a Print Statement

So what’s happening? Fortunately, we have this print statement that we can start to use as a rudimentary debugger.

If we try a positive number, we get the correct return value and the print statement. If we try a negative number, we get an incorrect return value and no print statement. This tells us that our if statement is working because it only triggers the print when the input is positive.

Great, but we still aren’t getting the correct return value for negative numbers. So, what do we know?

Well, we know that somehow the state variable is being overwritten regardless of input. Maybe it’s possible that the line where we set state to true isn’t actually grouped with the if statement.

Let’s try wrapping the if statement in brackets to ensure the state assignment is only executed during the positive input branch:

public static boolean isPositive(int num) {
    // Assume false
    boolean state = false;

    if (num > 0) {
        System.out.println("num is positive");
        state = true;
    }

    return state;
}

Ah! There we go. If we try passing in a negative value, we will never enter the if block. As a result, the state will never get reassigned, and we will get our proper return value.

Lessons Learned

So what are some lessons learned here? First, print statements are our friends. We can leverage them to isolate areas in code where problems can be detected. Also, they’re quick and dirty. They let us rapidly check the state of variables and other objects without requiring too much extra code.

Of course, we probably shouldn’t go putting print statements everywhere. They can quickly clog up code and hurt readability.

And while we’re on the topic of readability, the first snippet of code is an excellent example of poor style biting back. Though, I’ll probably get some nasty comments for that one.

In my opinion, we should always use braces on a code block regardless of how trivial it is. Eventually we’ll form a habit of it, and we’ll never look back! In fact, many IDEs will allow us to do this by default, so we never run into these kinds of issues.

Design by Contract

Alright, we’ve formally covered debugging! We didn’t go into debugger tools for the sake of scope, but we definitely touched on the main idea.

Now, let’s get into testing. In particular, let’s cover unit testing which is a special type of testing that checks the functionality of a “unit” of code. A unit is a small piece of code that can be isolated and tested independently.

In most cases, a “unit” is a method. But how do we know what to check for in a unit of code? That’s where Design by Contract comes in.

Example

Design by Contract (DbC) is a programming methodology which specifies rules for making assertions. In particular, DbC specifies the precondition and postcondition for operations like methods. These two rule sets specify the contract that must be upheld by the method.

To make sense of DbC, let’s take a look at an example:

/**
 * Returns factorial of a number.
 * 
 * Precondition: 0 <= num <= 12
 * Postcondition: return == num!
 */
public int factorial(int num) { ... }

In this example, we have the standard factorial method which we didn’t bother to implement. However, what makes it different is the DbC notes in the comment. In particular, we have a precondition and a postcondition.

Precondition

In the precondition, we specify what has to be true about the state of the class and the input for the method to behave properly. In this case, we don’t care about the class since this is probably more of a static method anyway.

That said, we do care what is passed into the method:

// Precondition: 0 <= num <= 12

On one end, it doesn’t make sense to compute a negative factorial, so we specify that in the precondition.

On the other end, we have some limitations in the size of an integer. If we accept numbers that are too large, our result will wraparound. We don’t want that, so we ask that inputs are never greater than 12.

That doesn’t mean we can’t call the factorial method with negative values or values greater than 12. We are stating that doing so is an error on the caller not the method.

Postcondition

Meanwhile, the postcondition tells us the state of the output and the class after running the method. Since we aren’t modifying any state variables, we made a rule about the expected output:

// Postcondition: return == num!

In this case, we are promising that the result is the factorial of the input. Simple enough!

Defensive Programming

To be clear, DbC does not mean that we ignore inputs outside of our precondition set. As good defensive programmers, we’ll make sure to report errors or exceptions for all bad inputs.

Likewise, DbC doesn’t ensure that we’ll always get good output on our methods either. The contracts themselves just allow us to begin building up a testing regiment. If we know what to expect at each end of a method, then we can begin testing them.

For more information, check out UNC’s brief introduction to Design by Contract.

JUnit Testing Basics

So, what have we covered so far?

Well, we kicked this lesson off with some basic debugging. To start, we looked at a method and determined its expected behavior. Then, we analyzed the solution and broke the method down into its branches.

To test those branches, we picked two data points—one for each branch. We then ran the method using each data point, and we analyzed the results. The results indicated that only one of the data points actually worked as intended.

From there, we leveraged the preexisting print statement to get an idea of where our solution was failing. Once we identified the problem, we reworked our code and retested the two data points.

A Look Back

After some debugging, we covered a lesson on Design by Contract.

To be clear, we typically won’t use DbC in the strict sense, but the concept applies nicely to testing. In fact, why don’t we try applying the DbC principles to the method we debugged? That way we can get more comfortable with the rules before we step into testing:

/**
 * Checks if the input is positive.
 *
 * Precondition: None
 * Postcondition: true if num > 0, false otherwise
 */
public static boolean isPositive(int num) {
    // Assume false
    boolean state = false;
 
    if (num > 0) {
        System.out.println("num is positive");
        state = true;
    }

    return state;
}

Here we can see that we don’t make any assumptions about the input. We will gladly accept the entire range of integer values as input. As for the postcondition, we promise that the output will be true for integers greater than 0 and false otherwise.

Now that we know our precondition and postcondition, we know exactly what to test, and we’ve demonstrated this during debugging.

Unfortunately, code doesn’t generally sit around untouched. Later, we might want to add another clause that specifies the behavior for 0. In cases like these, it helps to write automated tests which handle the sanity testing for us.

In other words, we don’t want to have to manually check that this method works every time we make a change.

Testing Using the Main Method

Fortunately, Java has a solution for this right out of the box. It’s a framework called JUnit, and it allows us to write test methods. But how do we write a test method? Before we dive into the syntax, let’s just think about that for a second.

Previously, if we wanted to test a method manually, what did we do? First, we tried to identify some inputs to tests the various branches in a method. Then we ran that method using those data points. In Dr. Java, this is trivial. We can call the method directly from the interactions pane using each data point.

However, if we’re using an IDE like Eclipse, we might have to manually write our test code into the main method. That is not a terribly fun way to go about testing, but it gets the job done for small projects. Let’s try it:

public class MyMathWorkshop {
    
    public static boolean isPositive(int num) {
        // Assume false
        boolean state = false;
 
        if (num > 0) {
            System.out.println("num is positive");
            state = true;
        }

        return state;
    }

    public static void main(String args[]) {
        boolean positiveTest = MyMathWorkshop.isPositive(5);
        boolean negativeTest = MyMathWorkshop.isPositive(-5);

        System.out.println("Positive Test: " + positiveTest);
        System.out.println("Negative Test: " + negativeTest);
    }
}

After a quick run, we’ll have our results! However, this method of testing is super tedious and not always possible. Fortunately, we can leverage JUnit testing.

Introducing JUnit

The beauty of JUnit testing is that all of the code in our main method can be extracted into a special test method. Even better still, we can swap those print statements for special assert methods. These assert methods allow us to check the actual result of our method call against some expected result. For example:

assertTrue(MyMathWorkshop.isPositive(5));

In this line, we assert that isPositive(5) returns true. If for some reason isPositive(5) returns false, the test will fail. As a side note, we could have written the test as follows:

boolean positiveTest = MyMathWorkshop.isPositive(5);
assertTrue(positiveTest);

In this example, we explicitly store the result of our test in a boolean variable. Then, we pass that variable into our test method.

This type of syntax is probably what we’re most familiar with. However, Java allows us to skip the local variable step altogether. Instead, we can pass a method call as a parameter to another method as seen in the first assertTrue example.

Both options are valid, so it’s really a matter preference. Option 1 can sometimes be more difficult to debug because both method calls share the same line. We’ll probably run into this issue as we debug code in the future.

JUnit Example

At any rate, back to testing! Now, we know how to use JUnit testing on our methods. Let’s go ahead and take a look at an example of a test file for our MyMathWorkshop class.

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

import org.junit.Test;

public class MyMathWorkshopTest {
    
    @Test
    public void isPositiveTest() {
        assertTrue(MyMathWorkshop.isPositive(5));
        assertFalse(MyMathWorkshop.isPositive(-5));
    }
}

There’s a lot of code here that we haven’t seen before. For starters, our test method has an annotation over it (@Test).

The annotation is metadata that the JUnit testing framework uses to identify test methods. In other words, we must mark all of our test methods with the @Test annotation.

Meanwhile, outside the class we have several import statements. These statements give us access to methods from the JUnit testing framework. There are a whole list of these test methods, but the main ones we will likely use are assertTrue, assertFalse, and assertEquals.

Executing Test Cases

In DrJava, running these types of files is as easy as hitting the test button after compiling our code. If successful, we should get a list of all the test methods and their results. Since we only have one test method, we should see a single passing test result highlighted in green. If the test failed, the line would get highlighted in red.

Other IDEs like Eclipse also do a great job of integrating testing with development, but we’ll be doing a deeper dive with those types of tools later.

As an alternative, we can write tests using the TestCase framework. Here we import junit.framework.TestCase and extend our class by it. This method is a bit cleaner, and it forces us to follow good naming conventions. However, we haven’t learned anything about inheritance yet, so we should avoid this method for now.

Code Coverage

At this point, we should feel pretty comfortable with testing and debugging code. All of the logic we have worked with so far has been pretty simple with the occasional branch case, so we might not see the full value of what we learned today.

However, as we move forward, we’ll start to tackle much more complicated concepts such as loops and data structures. Then we’ll need to look into code coverage to make sure we’re actually proving our methods do what we want them to do.

Code coverage is a software methodology which prioritizes tests that traverse every line of code. We actually achieved 100% branch coverage in our JUnit testing example above. If we decided to add our factorial method to the mix, then we would need to write some more tests.

Many IDEs provide static analysis tools that will actually tell us the percentage of code covered by our tests. In addition, they’ll tell us which lines are missing. In fact, Dr. Java now supports code coverage as one of its latest features.

Looking Forward

Since we have covered debugging and testing, we should be ready to take on a new challenge. Up next, we’re going to tackle loops. Make sure you study up on all our previous lessons as many of these topics will start to build on each other. In fact, loops add another level to control flow, so we’ll definitely want to get more comfortable with conditions.

As for now, make sure to share this lesson with your friends. If you’re really enjoying this series, why not subscribe to The Renegade Coder. That way, you’ll never miss another article.

Series Navigation← Control Flow Syntax and Design in JavaLoop Syntax and Design in Java →
Advertisements

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.