I refer to an IDE in this article that not everyone may be familiar with - RAD. RAD stands for Rational Application Developer and is an IDE provided by IBM. It is pretty much a commercialized version of Eclipse. If you think Eclipse every time you see a reference to RAD, you will be fine. Nothing in this is specific to RAD and I could have substituted "Eclipse" in place of "RAD".
Unit testing is testing each component of a piece of software as a unit of work. The percentage of code which is covered as part of the test is typically called code coverage. Unit tests help ensure that the code is working as it is supposed to. It also acts as living documentation on how to use the code. Lastly, unit testing provides a protective blanket for refactoring/updating code. Unit tests are inherintly bug repellant. Unit tests should be:
- a part of the software development life cycle
- written while coding - before or after is a debate, but you should at least consider how a test will work before you write the code
- used to test the methods we have written - we know what to expect from a method that we've written, but we have to assert that the method result and expected result match our expectation/requirement.
Unit tests should be in the same project as the code that it is testing. However, it should reside in a different source directory - for example src/test/java
instead of src/main/java
. The tests themselves should reside in the same java package as the code that it is testing. This will give the JUnit test access to the protected methods within the class under test.
Your unit tests should concentrate the tests on the non-trivial code that you've written. There is no need to test the built in functionality of Java itself, nor the functionality of various third party code. It is best to trust that provided functionality has been tested already.
JUnit 3 and 4 are both still used, but we will concentrate on JUnit 4 for this feature list. Equivalents for JUnit 3 are available, but may require more research on how to use them.
Annotation | Description |
---|---|
@Test |
identifies a method as a test method |
@Test (expected = Exception.class) |
identifies a method as a test method that is expected to throw an exception of type Exception |
@Test (timeout = 100) |
identifies a method as a test method that should not take longer than 100 milliseconds |
@Before |
identifies a method that should be executed before each test. It is generally used to setup the test environment |
@After |
identifies a method that should be executed after each test. It is generally used to tear down the test environment |
@BeforeClass |
identifies a method that should be executed before any tests are run (not in between tests). This method should be marked static |
@AfterClass |
identifies a method that should be executed after all tests are run (not in between tests). This method should be marked static |
@Ignore |
identifies a test method should be ignored. This should only be used when code is actively in development and should not be committed to source control |
Statement | Description |
---|---|
fail(String) |
Forces a test method to fail |
assertTrue(String message, boolean condition) |
Checks to see if the condition is true. If it fails, the message is printed out |
assertFalse(String message, boolean condition) |
Checks to see if the condition is false. If it fails, the message is printed out |
assertEquals(String message, Object expected, Object actual) |
Checks to see if the actual object value matches the expected object value. NOTE: This will not work for arrays as only the reference is checked, not the values within the array |
assertEquals(String message, double expected, double actual, double tolerance) |
Checks to see if the actual value is within tolerance of the expected value. If it is not, then the message is printed out |
assertNull(String message, Object object) |
Checks to make sure the object is null. If it isn't then the message is printed out |
assertNotNull(String message, Object object) |
Checks to make sure the object is not null. If it is then the message is printed out |
assertSame(String message, Object expected, Object actual) |
Checks to make sure the actual object is the expected object. This is not a check of the value, the objects have to be exactly the same |
assertNotSame(String message, Object expected, Object actual) |
Checks to make sure the actual object is not the expected object. This is not a check of the value, the objects must not be the same object |
All of the above assertion statements use String message
as the first argument. Technically this argument is optional. In practice it is good form to use the argument to provide meaningful messages for the next developer to identify problems.
In JUnit 4, the assert statements are now included as static imports. Out of the box, RAD does not handle static imports very well. There are two ways to help RAD figure out where the assert methods are:
- You can import the
org.junit.Assert
class and then use the static assert methods on that class - ie.Assert.fail("This should fail")
- You can add a content assist for JUnit Asserts. This can be done by opening the Preferences window via
Window -> Preferences
. Then selectJava -> Editor -> Content Assist -> Favorites
. Click on theNew Type...
button to add theorg.junit.Assert
type. This will help RAD find the assert methods through the Content Assists (Ctrl+Space, etc)
Suppose we have a Calculator class...
package engine;
/**
* This class is the sample class under test
*/
public class Calculator {
public int add(int value1, int value2) {
return value1 + value2;
}
public int subtract(int value1, int value2) {
return value1 - value2;
}
public int multiply(int value1, int value2) {
int result = 0;
for (int i = 0; i < value2; i++) {
result += value1;
}
return result;
}
public int divide(int value1, int value2) {
return value1 / value2;
}
}
To create a new JUnit test within RAD, right-click on your test folder and select New...
and then JUnit Test Case
. If you haven't already added JUnit to your classpath then RAD will ask you which version and do it for you. When in the wizard to create the test, it will ask you what the class is under test. Selecting that will tell RAD to stub out the test methods with tests that fail.
Here is an example of a JUnit Test Case for the Calculator class...
package engine;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Ignore;
import org.junit.Test;
/**
* Test class for {@link Calculator}.
*/
public class CalculatorTest {
/**
* Test method for {@link engine.Calculator#add(int, int)}.
*/
@Test
@Ignore
public final void testAdd() {
fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link engine.Calculator#subtract(int, int)}.
*/
@Test
@Ignore
public final void testSubtract() {
fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link engine.Calculator#multiply(int, int)}.
*/
@Test
public final void testMultiply() {
// make sure that 5 * 10 = 50 according to our implementation
Calculator calc = new Calculator();
int actual = calc.multiply(5, 10);
assertEquals("5 * 10 should equal 50", 50, actual);
}
/**
* Test method for {@link engine.Calculator#divide(int, int)}.
*/
@Test
@Ignore
public final void testDivide() {
fail("Not yet implemented"); // TODO
}
}
Running this unit test will result in the testMultiply
method being executed and the other three being skipped because they have the @Ignore
tag on them. All of the tests pass, but Calculator class has a bug in the multiply method.
Unit tests can be used to replicate the problem and resolve them. Someone from the test team has reported that sometimes the calculations are coming up as 0 when it tries to multiply by negative values. So let's modify our JUnit test a little to see how it fails. Let's update our testMultiply
method to handle additional scenarios.
public class CaclulatorTest {
...
/**
* Test method for {@link engine.Calculator#multiply(int, int)}.
*/
@Test
public final void testMultiply() {
// make sure that 5 * 10 = 50 according to our implementation
Calculator calc = new Calculator();
int actual = calc.multiply(5, 10);
assertEquals("5 * 10 should equal 50", 50, actual);
// try multiplying -4 and 5 - it should result in -20
assertEquals("-4 * 5 should equal -20", -20, calc.multiply(-4, 5));
// try multiplying 5 and -2 - it should result in -10
assertEquals("5 * -2 should equal -10", -10, calc.multiply(5, -2));
// try multiplying -4 and -3 - it should result in 12
assertEquals("-4 * -3 should equal 12", 12, calc.multiply(-4, -3));
}
...
}
When this is executed, JUnit now fails this test saying 5 * -2 should equal -10 expected: <-10> but was: <0>
. We seem to have found the issue reported - when the seconds parameter is negative it is returning 0. We can now fix our implementation in this scenario.
public class Calculator {
...
public int multiply(int value1, int value2) {
int result = 0;
if (value2 >= 0) {
// if the second value is positive, then add the values
for (int i = 0; i < value2; i++) {
result += value1;
}
} else {
// if the second value is negative, then subtract the values
for (int i = 0; i > value2; i--) {
result -= value1;
}
}
return result;
}
...
}
Running our JUnit test again shows us that all tests pass - we've fixed the bug and now the code is more resilient for future updates.
Although the example above is fairly simple, adding more assert method calls within the test method could get to be extremely repetative. Fortunately, JUnit gives parameterized tests to combat this repetativeness. Take a look at the example below of how the test could change.
package engine;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
/**
* Test class for {@link Calculator}.
*/
@RunWith(Parameterized.class)
public class CalculatorTest {
private int value1;
private int value2;
private int result;
public CalculatorTest(int value1, int value2, int result) {
this.value1 = value1;
this.value2 = value2;
this.result = result;
}
/**
* Creates the test data that will be passed into the constructor
*/
@Parameters
public static Collection<Object[]> data() {
Object[][] data = new Object[][] { {5, 10, 50}, {-4, 5, -20}, {5, -2, -10}, {-4, -3, 12} };
return Arrays.asList(data);
}
/**
* Test method for {@link engine.Calculator#multiply(int, int)}.
*/
@Test
public final void testMultiply() {
Calculator calc = new Calculator();
assertEquals("The multiplication result didn't match what was expected.", result, calc.multiply(value1, value2));
}
}
To make the test parameterized, we had to change a few things:
- Add the annotation
@RunsWith(Parameterized.class)
to the class definition - Add private class level attributes to hold the parameters
- Add a constructor that took in the number of parameters we wanted to supply
- Add a method annotated with
@Parameters
(data()
in this case) to generate and provide the parameters - Updated the
testMultiply()
method to use the class level attributes
- The inspiration for this text came from Skill Guru and Lars Vogel
See more notes at http://jsturdevant.roughdraft.io/
Written with StackEdit.