By now, you are past the basics and should be familiar with developing Java applications using the Eclipse IDE. Although you must be feeling very confident with your recent acquired skills, before we go any further, you should know that the more you code, the higher are the chances of you introducing a bug into your code. Even with all the facilities provided by Eclipse, things are bound to go wrong as Java applications scale up. So, we need to build a safety net whenever our application code starts to become nontrivial to manage. One way of doing this is to test our programs, and testing is one of the things that we are going to cover in this chapter.
In an ideal world, all problems detected by our tests would be easy to track down and fix. Nevertheless, this is not always the case. It turns out that nasty bugs might make their way into our code. As mentioned, having a good set of tests helps us to spot problems, however, sometimes the feedback provided by the test set is not enough, and we need to dig deeper to uncover problems. The act of understanding why the software is not behaving properly and working out a fix for it is called debugging. Debugging is covered later in this chapter.
When it comes to testing and debugging Java programs, Eclipse has got you covered. It comes with built-in features for automating the creation and execution of JUnit tests, and it features an impressive Java debugger.
We will cover the following topics in this chapter:
Before describing JUnit, we believe that some words about unit testing are in order. When carrying out unit testing activities, emphasis is given to small chunks of code. In other words, you are not trying to cover the whole program with a single test. Rather, you are testing the foundations of your programs by writing a suite of tests per class, one or more tests per method. Writing tests while you are working on the code at hand has a host of benefits. For instance, it is easier to find bugs when unit testing. This is the case because each unit test should evaluate exactly one functional unit, which makes the scope one has to hunt for bugs significantly smaller than searching the whole program for bugs. However, according to Robert C. Martin, to reap the full benefits of unit testing, your test suite has to comply with the following principles (also known as the FIRST acronym):
Now that you have a basic understanding of the philosophy that backs up unit testing, you are ready to take the next step towards writing your own tests—learning how to use a unit testing framework. Since JUnit is arguably the most popular testing framework in the Java community, we will cover it here.
More specifically, JUnit is an open source testing framework, which was initially contrived by Kent Beck and Erich Gamma (www.junit.org). As of the time of this writing, the latest stable version of this framework (and the one bundled with the IDE) is Version 4.10. Due to the fact that Erich led the design of the Eclipse Java Development Tools (JDT), JUnit is well-integrated into the IDE. Eclipse already ships with a bundled JUnit JAR. So, as long as you are planning on using JUnit 4.10, there is not much to say about installing it. However, it is also possible to use a different JUnit version. The steps for setting up the default and a non-default JUnit JAR are covered in the following sections.
Although the duo, JUnit and Eclipse, supports and automates most of the testing process, you still need to write your own test files manually. Since Eclipse leaves the organization of your test files up to you, a word about how to organize these files is in order.
JUnit-based test files can be organized in the following way. Suppose your application has a class named MyAppClass
, and you want to test it. To do so, you need to place your tests in another class named MyAppClassTest
. Usually, test classes are kept separated from regular code. The convention is placing them into a subpackage called test. So, let's say that MyAppClass
is in a package called org.myapp
, tests related to this class need to be placed in the subpackage named org.myapp.test
. As for the tests themselves, they are grouped in MyAppClassTest
in the form of Java methods with some slight modifications that we will describe shortly. An overview of this typical organization is shown in the following screenshot, which illustrates this organization using a hypothetical Java project named MyTestProject
:
It is worth mentioning that neither JUnit nor Eclipse enforces this naming convention. The creation of separate folders/subpackages for tests is not mandatory either. However, following these conventions is a good programming practice. It is also worth mentioning that another widely used convention to organize test code is the maven convention. By following this convention, you need to create separate folders for regular Java code and test code, src/main/java and src/test/java
. Also, the test classes are placed in the same package in which the classes they test are placed.
In order to use JUnit with Eclipse, you need to add junit.jar
to your project's build path. To do this, right-click on your project, and navigate to Build Path | Add Libraries…, as shown in the following screenshot:
The following screenshot shows the options listed by Eclipse. Select the second option JUnit. You can select either JUnit Version 3 or Version 4.
Throughout this chapter we are going to use JUnit 4 (4.10 to be more specific); so go ahead and select JUnit 4 as shown in the following screenshot:
There is a striking difference between JUnit 3 and JUnit 4, mostly due to the fact that JUnit 4 heavily relies on the new features introduced by the most recent versions of Java. To be more specific, the main difference between JUnit 3 and JUnit 4 is that the latter relies on annotations rather than hierarchies and method-name conventions. If you are familiar with JUnit 3, but have not used JUnit 4 yet, you will notice that the latter is simpler and easier to use than the former.
By choosing JUnit 4, Eclipse adds JUnit 4.10 to your project's build path. If you need to use a different JUnit version, you can download that particular version from JUnit's website and add it to your project's build path. To do that, download the desired version from JUnit's website, right-click on your project name, and navigate to Build Path | Configure Build Path…, as illustrated in the following screenshot:
The project properties window will appear, showing the project's build path current configurations (shown in the following screenshot). Select the Libraries tab, click on Add External JARs…, locate and select your recently downloaded JAR, and then click on OK. Click on OK again to close the properties window. Done, now you have JUnit up and running. We are all set to create JUnit-based tests. It is worth mentioning that through the project properties window you can also add JUnit 4 by clicking on the Add Library… option. Selecting this option opens the pop-up window shown in the earlier screenshot.
Adding a new JAR to the build path can be done in a myriad of ways. In this section we just covered two. If you followed the first approach, the JUnit JAR file is going to be placed under JUnit 4 as shown in (a) in the following screenshot. Otherwise, the JAR file is going to appear under Referenced Libraries as shown in (b). Keep on reading to discover an even easier approach for setting up JUnit.
Using JUnit, it is remarkably simple to write repeatable tests. Actually, from day one, being simple to use was a key design decision made by Kent and Erich. According to them, if JUnit was not easy to learn and execute, programmers would not actually use it.
Basically, when you need to test something using JUnit, you perform these basic actions:
@org.junit.Test
.org.junit
. Assert statically, and invoke one of the methods of the Assert family (assertTrue
, assertEquals
, and so on) to compare the result of your test with the expected result.The JUnit framework has several other features, but these are the ones you will need to understand in order to get started. So, let's dive into the technical details of these basic features by testing a Java class that implements a basic calculator.
First, we need a new Java project for our calculator class. So, inside Eclipse navigate to File | New | Java Project. To give our project a name, type BasicCalculatorProject
in the Project name field, and then click on Finish. You might be prompted to switch to the Java Perspective. If this happens, answer Yes. After performing all these steps, the Package Explorer view should show your new Java project. However, if something goes wrong and you can run into problems, go back to the previous chapter and review how to create a Java project.
Inside our recently created Java project, create a new package. To create a package named chapter3.basiccalc
, select the src
folder, right-click on it, and navigate to New | Package. Enter the package name, and click on the Finish button.
Let's create a class named BasicCalculator
. Right-click on your recently created package and navigate to New | Class. Our class under test is as follows:
package chapter3.basiccalc; public class BasicCalculator { private static int[] tempVars; //acts as an array of temp variables private static int result; public void add(int n) { result += n; } public void subtract(int n) { result -= n; } public void addResultToTempAt(int index) { tempVars[index] = result;// BUG: tempVars wasn't initialized properly } public void squareRoot(int n) { //not implemented yet } public void divide(int n) { result /= n; } public void multiply(int n) { result = n * n; } public void clear() { result = 0; } public void clearTemps() { for (int i =0; i < tempVars.length; i++) { tempVars[i] = 0; } } public int getResult() { return result; } }
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub.com/support and register to have the files e-mailed directly to you.
Our calculator implementation contains two faults, but do not mind about them yet. Using a buggy implementation will make our example more interesting.
The purpose of the BasicCalculator
class is to perform elementary arithmetic operations (addition, subtraction, multiplication, and division) and calculate the square root; while storing the result after each operation. Apart from the result variable, an array is used to store preliminary results; making it possible to reuse it in subsequent calculations. Methods for reusing the values stored in this array are not implemented yet. So far, our calculator can only store preliminary results (which are kept in the tempVars
array) and clean its memory (by assigning zero to all elements of tempVars
). Further, notice that the method that should calculate the square root is not implemented either.
Well, our class should calculate the square root, however, as you saw, this particular feature has not been implemented yet. Do not worry about that, we did not implement this particular feature for demonstration purposes. Later in this chapter, we will show you how to make JUnit skip a test case for which either there is no implementation or the current implementation has been acting up.
Since we are sticking to the naming convention we presented in the previous section, before testing BasicCalculator
, you should create a package called chapter3.basiccalc.test
. The class containing all test cases should be created inside this package.
Now that you have set your Java project up and dealt with all scaffolding around your test code, you can either add JUnit to your project build path following the aforementioned directions, or keep reading and discover an even easier way of adding JUnit to the build path (as we mentioned, there are many ways of doing operations in Eclipse).
Right-click on BasicCalculator
and navigate to New | JUnit Test Case. The window shown in the following screenshot will appear:
After selecting the JUnit Test Case option, a new window will pop up with the name of the test case already filled out (BasicCalculatorTest
). It also shows the class under test (BasicCalculator
). You only need to type the test package name in the Package field. So go ahead and type chapter3.basiccalc.test
as we did in the following screenshot (or click on the Browse button and select the desired package).
Notice that we uncheck the setUp() and tearDown() options. We are going to explain what these terms mean shortly. But for the time being, we do not want Eclipse to automatically generate these methods for us. After unchecking these options (in case they are not unchecked by default), click on Next and select the methods that you want to test, as demonstrated in the following screenshot. Here Eclipse is prompting you to select the methods for which it should generate stubs.
When you click on Finish, if you have not added JUnit to your project build path yet, Eclipse will prompt you for adding JUnit 4 to the build path (as shown in the following screenshot). You should respond by clicking on OK.
Eclipse will then generate the following code for you:
package chapter3.basiccalc.test; import static org.junit.Assert.*; import org.junit.Test; public class BasicCalculatorTest { @Test public void testAdd() { fail("Not yet implemented"); } @Test public void testSubtract() { fail("Not yet implemented"); } @Test public void testAddResultToTempAt() { fail("Not yet implemented"); } @Test public void testSquareRoot() { fail("Not yet implemented"); } @Test public void testDivide() { fail("Not yet implemented"); } @Test public void testMultiply() { fail("Not yet implemented"); } @Test public void testClear() { fail("Not yet implemented"); } @Test public void testClearTemps() { fail("Not yet implemented"); } }
As you can see, Eclipse generated a lot of boilerplate code automatically. To be more specific, Eclipse generated all test methods (one for each method of our class under test), annotated then with org.junit.Test
(@Test
), and statically imported the assert methods.
The @Test
annotation is used to indicate to JUnit that the annotated method should be run as a test case. Prior to running each test case, JUnit creates an instance of the class and then invokes the test case method on that instance. Later in this chapter, we will see how JUnit allows programmers to take advantage of this characteristic to initialize the context for test method that needs objects created before they can be executed.
If a test throws an exception, JUnit reports it as a failure. As you might have already figured out, all test method stubs generated by Eclipse are failing test methods. In fact, our test methods are failing because they invoke the fail()
method from org.junit.Assert
, which signals the failure of a test method by throwing an AssertionError
.
In order to execute a JUnit-based test class, right-click on it and navigate to Run As | JUnit Test, as shown in the following screenshot:
Following the approach shown just now, try to run BasicCalculatorTest
. The results of the tests are shown in the following screenshot in the JUnit Runner view. Whenever you run a JUnit-based class, this view is shown in the current perspective. This view shows you a list of failures and the test suite as a tree. Failing tests are indicated with a red bar. (as shown in the following screenshot, we have eight failing tests). The next sections provide more information on each toolbar of this window.
By now, you should be used to the fact that you can perform the same operation in Eclipse in different ways. There is no exception when it comes to running test cases. Here are some of the ways you can run your test cases:
In order to check out what a passing test looks like, comment the body of the method testAdd()
as in the following code snippet, and using the approach described just now, run only testAdd()
:
@Test public void testAdd() { //fail("Not yet implemented"); }
You should see a green bar; as illustrated in the following screenshot. To be more specific, it shows that testAdd()
is a passing test method and the other test methods were not executed.
Before implementing tests that really evaluate the behavior of our calculator, it is worth taking a closer look at the JUnit Runner view, because you are going to be interacting with it throughout this chapter.
After executing the JUnit-based classes, the JUnit Runner view is shown. In addition to indicating whether the recently run tests succeeded or failed, this view also shows why the failing tests failed and some useful toolbar commands. The JUnit Runner view has two panes, the tests pane and the failure trace pane. The tests pane lists all the executed tests as a tree. Upon selecting a failed test, the failure trace pane shows information on why the selected test failed. The following table presents a description of each toolbar command in the tests pane:
Icon |
Description |
---|---|
Moves the selection to the next failing test | |
Moves the selection to the previous failing test | |
Shows only failing tests, if any | |
Keeps the test list from scrolling | |
Re-runs all tests | |
Re-runs only all failing tests | |
Stops the current execution | |
Shows test run history |
The following table describes the options available in the Failure Trace pane. To navigate from a certain failure to the related source code, double-click on the corresponding line in the Failure Trace pane.
Icon |
Description |
---|---|
Filters stack trace by removing unwanted stack frames from it | |
Compares the actual and the expected results of String comparisons |
So far, we have not typed much; Eclipse has done everything for us. However, now it's time for us to do some typing. Aimed at implementing our test cases, we are going to take small steps. First, we need to comment out test cases that have not been implemented yet. It turns out that JUnit provides a better way to disable unfinished test methods, by annotating them with @Ignore
. Methods annotated with @Test
and @Ignore
will not be executed by JUnit at all. JUnit treats them as if they were commented out. When annotating a method with @Ignore
, you can also provide a string that explains why the underlying test method is being ignored.
In order to use @Ignore
, you have to import org.junit.Ignore
. Once imported, @Ignore
can be used to annotate the methods, as illustrated in the following code. In the following code, we are telling JUnit to ignore testSubtract
, because it has not been properly implemented yet. Go ahead and mark all methods but testAdd()
with @Ignore
. Executing our test suite now will result in no failures.
@Ignore("Not implemented yet") @Test public void testSubtract() { fail("Not yet implemented"); }
Now that the other test cases won't get in our way, let's start implementing testAdd()
. Fortunately, writing unit tests is very straightforward. You just need to call chunks of your application's code, get the results back, and then check if they are exactly what you expected. Instead of having to write lots of if statements to compare the results when using JUnit, you can rely on the assert methods. Using assert methods, you specify the expected outcome, and then pass the actual result from invoking your application code.
For example, we can implement our testAdd()
method by performing simple addition operations as shown in the following code. First, we need to create an instance of BasicCalculator
. Second, we invoke the chunk of code that we are trying to test, namely, the add()
method. Finally, we use an assert method to evaluate the result of invoking the add()
method by passing 2
as parameter twice. The assertEquals()
method expects getResult()
to return 4
. Upon running our test case, we can see that the method under test seems to be working properly since getResult()
returned 4
, as expected. Here is how we implemented testAdd()
:
@Test public void testAdd() { BasicCalculator calculator = new BasicCalculator(); calculator.add(2); calculator.add(2); // result should be 4 assertEquals(4 /*expected*/, calculator.getResult() /*actual*/); calculator.clear(); }
Before implementing more tests, a few more words about assert methods are in order. As you can see in the previous example, assertEquals()
takes two parameters, the expected result and the value to check against the expected result. In our code, we are referring to this method through static import (check out the static import statement that Eclipse autogenerated for us). If we were to use assertEquals()
without statically importing it, we would have to do more typing: Assert.assertEquals(4, calculator.getResult())
.
The test for subtraction operations is similar to the one for addition operations. Again, we create an instance of the class under test, invoke the chunk of code under test, and check if the invoked code yielded the expected outcome. Go ahead and implement testSubtract()
as shown in the following code snippet. Note that our testSubtract()
method relies on add()
to set the stage for subtract()
. More specifically, add()
is invoked to store the value 35
into the calculator's result variable. Then we subtract 5
from the result and use an assert
method to evaluate whether after performing these arithmetic operations the result contains the value 30
.
@Test public void testSubtract() { BasicCalculator calculator = new BasicCalculator(); calculator.add(35); calculator.subtract(5); assertEquals(30, calculator.getResult()); calculator.clear(); }
As you can see, all tests start by creating an instance of BasicCalculator
(the one upon which the methods under test are invoked) and conclude with an invocation to calculator.clear()
(in order to clear the value stored in result, so that it will not tamper with the results yielded by the other test methods). It is always a good idea to eliminate duplicated code from both your application and test code. We can extract this duplicated code into a method that does all the initialization and configuration of the test environment, and a method that contains the cleanup code. In JUnit 4, in order to have a method executed before each test case, simply denote it with @Before
(org.junit.Before
). Here is how we can remove part of the duplicated code:
private BasicCalculator calculator; @Before public void setup() { calculator = new BasicCalculator(); } @Test public void testAdd() { calculator.add(2); calculator.add(2); // result should be 4 assertEquals(4/*expected*/, calculator.getResult()/*actual*/); calculator.clear(); }
Note that calculator
is now an instance variable, and neither testAdd()
nor testSubtract()
(not shown in the code snippet) need to instantiate a local version of BasicCalculator
; they simply use the calculator
instance variable. Also note that all initialization code was moved to the setup
method, which JUnit makes sure to invoke before executing each test method. Normally, methods that contain the initialization code needed for exercising the program under test are named setup
. Likewise, methods containing the cleanup code are named teardown
. Given that each test case now uses its own instance of BasicCalculator
, if result
were not a static variable, we would not need to invoke clear()
on those instances. So, we would be able to safely get rid of all calls to clear()
. However, since it is a static variable, we still need to invoke clear()
after each test method.
In our example, the cleanup code should be extracted into a method denoted with @After
. By doing so, this method will be executed after each test case. Go ahead and implement the cleanup code as follows:
@After public void teardown() { calculator.clear(); calculator = null; //this isn't really necessary }
Looking closely at our code, we can see that we do not need a new BasicCalculator
object before executing our test methods. We can re-use one instance of BasicCalculator
as long as we invoke clear()
on that instance, after the execution of each test method. You might be wondering how to achieve this using JUnit. Actually, it is quite straightforward. JUnit provides an annotation that allows us to signal what methods must be executed prior to running all the test methods. The annotation that makes this possible is @BeforeClass
. To be annotated with @BeforeClass
, a method must be declared as public as well as static, and it can take no parameters. We no longer need our previous setup()
method, so let's tweak it so that it will be executed only once for all the test cases (note that our teardown()
method is still invoked after each test method to reset the result of calculator):
private static BasicCalculator calculator; @BeforeClass public static void setup() { calculator = new BasicCalculator(); } @After public void teardown() { calculator.clear(); }
Now, let's move on to more challenging arithmetic operations. Let's get down to tackling the test of the multiply()
and divide()
methods. Luckily for us, testing multiply is straightforward, as shown in the following code snippet:
@Test public void testMultiply() { calculator.add(10); calculator.multiply(3); assertEquals(30, calculator.getResult()); }
Although testMultiply()
looks quite simple, upon running it, we discover a bug. As shown in the following screenshot, instead of returning 30
, getResult()
returns 9
. Note how the error message uses the expected value and the actual value to inform us that our test failed.
Puzzled, we take a look at the implementation of multiply to discover that rather than multiplying result
by the parameter n
, we are multiplying n
by itself and assigning this value to result
. Fortunately, the fix is easy:
public void multiply(int n) { result *= n; }
As you can see, even simple test methods are effective weapons against the bugs that lurk in our code. Nevertheless, so far we just wrote test methods that exercise the happy path of the code of our BasicCalculator
. Sometimes we need to write test methods that exercise edge cases of the chunk of code under test. In Java, inputs that cause a certain method to throw an exception can be considered edge cases. For example, an edge case that our calculator has to cope with is division by zero.
In Java, an attempt to divide an integer by zero will lead to an ArithmeticException
. If it is expected from our calculator to throw an ArithmeticException
whenever there is a division by zero, we need to explicitly state this expectation in our tests.
Fortunately, JUnit provides a way to deal with these edge cases and the associated expected exceptions effectively. The @Test
annotation supports two optional parameters. The first, expected
, specifies that a test method should throw an exception. If the underlying method throws no exception or the thrown exception is different than the one specified, the test fails.
For instance, we can use this annotation to implement an edge case of our testDivision()
method as in the following code. It specifies that the method divide()
from BasicCalculator
should throw an exception (ArithmeticException
) whenever a division by zero takes place. Note that we do not need to use any assert method since the only outcome that we are expecting is an ArithmeticException
. It is worth mentioning that the class passed as parameter to expected
must be a subclass of java.lang.Throwable
.
@Test(expected=ArithmeticException.class) public void testDivide() { calculator.add(10); calculator.divide(0); }
There is yet another way of achieving the same behavior. You can go about declaring expected exceptions through JUnit's ExpectedException
rule. First, let us give you a crash course in JUnit rules.
In earlier versions of the test framework, it was not possible to modify or augment JUnit's internal knowledge about a given test suite. Using the current version, we can use rules to modify test results, change a test in a certain way before running it, and add information to test results. We will use the @Rule
annotation to inform JUnit which rules to apply to the underlying test.
By using ExpectedException
in conjunction with @Rule
, you can specify in-test expectations regarding the exceptions that should be thrown for a particular test method. It takes more typing than expected, however, as shown here, you have more control over what is expected. For instance, you can even specify the message what the expected exception should contain.
@Rule public ExpectedException thrown = ExpectedException.none(); @Test public void testDivide() { thrown.expect(ArithmeticException.class); thrown.expectMessage("/ by zero"); //specify the expected message calculator.add(10); calculator.divide(0); }
As for the second optional parameter of @Test
, timeout
, it causes a test method to fail if it takes longer than a specified amount of milliseconds to execute. To give you an example, let's say that we implemented testSquareRoot
as an infinite loop, which fails after 1 second. Upon running the code below you should get a red bar along with a java.lang.Exception
containing the following message: "test timed out after 1000 milliseconds".
@Test(timeout=1000) public void testSquareRoot() { while(true); }
Okay, by now, we believe that you got the gist of writing test methods using JUnit and Eclipse. We even were able to uncover a bug in our calculator. Granted, it was a very simple, easy-to-spot bug.
We are sorry to say that not every bug is simple to uncover. Sometimes you might get a red bar that leaves you at a loss to figure out how to fix the problem. In other words, when a test fails, and you just cannot figure out why, even after spending quite some time staring at the code, you need to turn to a debugger. To illustrate this somehow, let's implement the test method, testAddResulToTempAt
. The addResultToTemp
method should perform the following operation: add the current value of store in the variable result to a position in the tempVars
array, according to the index passed as parameter. During the implementation of this test method, we realized that we did not implement a method to get this array of temporary results back. When implementing unit tests, realizing that you forgot something happens more often than not. To set this matter right, we add a simple get method, named getTemps
, which simply returns the variable tempVars
.
public int[] getTemps() { return tempVars; }
As for our test method, which uses our recently implemented getTemps
, we implemented it as the following:
@Test public void testAddResultToTempAt() { calculator.add(9); calculator.addResultToTempAt(1); int[] temps = calculator.getTemps(); assertEquals(9, temps[1]); }
As you can see, we once again use add to store the value 9
into the variable result
. Afterwards, in order to store 9
into the slot number 1
of the temporary memory, we invoke addResultToTempAt
and pass 1
as parameter. The expected behavior is that the array that represents the calculator's temporary memory now contains the value 9
at index 1
. However, the test fails (as shown in the following screenshot). Instead of the expected behavior, we get a NullPointerException
. The only clue we got from observing the Failure Trace pane is that the exception is thrown at line number 16 of our BasicCalculator
class. Just by looking at this line there is no way you can guess what is wrong (well, it also depends on your Java experience). It is for problems like this that the debuggers exist. So, we will not spoil the fun of discovering the root cause of this problem. Rather, we are going to go over the tools of the trade. That is, we are going to explain how to use Eclipse built-in Java debugger so that you can find and fix this bug by yourself.