- Mastering Unit Testing Using Mockito and JUnit
- Sujoy Acharya
- 7027字
- 2021-12-08 12:38:35
Working with JUnit 4++
This section explores the advanced features of the JUnit 4 framework and includes the following topics: parameterized test, Hamcrest matchers and assertThat, assumption, theory, timeout, categories, rules, test suites, and tests order.
Ignoring a test
Suppose a failing test blocks you to check-in a mission critical code, and you come to know that the owner of the code is on a vacation. What do you do? You try to fix the test or just comment out or delete the test to proceed with your check-in (committing files to a source control such as SVN), or you wait until the test is fixed.
Sometimes we comment out tests because the feature is not developed. JUnit came up with a solution for this. Instead of commenting a test, we can just ignore it by annotating the test method with @Ignore
. Commenting out a test or code is bad as it does nothing but increases the code size and reduces its readability. Also, when you comment out a test, then the test report doesn't tell you anything about the commented-out test; however, if you ignore a test, then the test report will tell you that something needs to be fixed as some tests are ignored. So, you can keep track of the ignored test.
Use @Ignore("Reason: why do you want to ignore?")
. Giving a proper description explains the intention behind ignoring the test. The following is an example of, where a test method is ignored because the holiday calculation is not working:
@Test @Ignore("John's holiday stuff failing") public void when_today_is_holiday_then_stop_alarm() { }
The following is a screenshot from Eclipse:
You can place the @Ignore
annotation on a test class, effectively ignoring all the contained tests.
Executing tests in order
JUnit was designed to allow execution in a random order, but typically they are executed in a linear fashion and the order is not guaranteed. The JUnit runner depends on reflection to execute the tests. Usually, the test execution order doesn't vary from run to run; actually, the randomness is environment-specific and varies from JVM to JVM. So, it's better that you never assume they'll be executed in the same order and depend on other tests, but sometimes we need to depend on the order.
For example, when you want to write slow tests to insert a row into a database, then first update the row and finally delete the row. Here, unless the insert function is executed, delete or update functions cannot run.
JUnit 4.11 provides us with an @FixMethodOrder
annotation to specify the execution order. It takes enum MethodSorters
.
To change the execution order, annotate your test class using @FixMethodOrder
and specify one of the following available enum MethodSorters
constant:
MethodSorters.JVM
: This leaves the test methods in the order returned by the JVM. This order may vary from run to run.MethodSorters.NAME_ASCENDING
: This sorts the test methods by the method name in the lexicographic order.MethodSorters.DEFAULT
: This is the default value that doesn't guarantee the execution order.
We will write a few tests to verify this behavior.
Add a TestExecutionOrder
test and create tests, as shown in the following code snippet:
public class TestExecutionOrder { @Test public void edit() throws Exception { System.out.println("edit executed"); } @Test public void create() throws Exception { System.out.println("create executed"); } @Test public void remove() throws Exception { System.out.println("remove executed"); } }
Run the tests. The execution order may vary, but if we annotate the class with @FixMethodOrder(MethodSorters.NAME_ASCENDING)
, the tests will be executed in the ascending order as follows:
@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestExecutionOrder { … }
The following Eclipse screenshot displays the test execution in the ascending order:
Learning assumptions
In multisite projects, sporadically, a date or time zone tests fail in a local CI server but run fine in other servers in a different time zone. We can choose to not run those automatic tests in our local server.
Sometimes our tests fail due to a bug in a third-party code or external software, but we know that after some specific build or version, the bug will be fixed. Should we comment out the code and wait until the build is available?
In many projects, Jenkins (for test automation) and SONAR (for code-quality metrics) run in a server. It has been observed that due to low resources, the automatic tests run forever when SONAR is processing and the tests run simultaneously.
JUnit has the answer to all these issues. It recommends using an org.junit.Assume
class.
Like Assert
, Assume
offers many static methods, such as assumeTrue(condition)
, assumeFalse(condition)
, assumeNotNull(condition)
, and assumeThat(condition)
. Before executing a test, we can check our assumption using the assumeXXX
methods. If our assumption fails, then the assumeXXX
methods throw AssumptionViolatedException
, and the JUnit runner ignores the tests with failing assumptions.
So, basically, if our assumption is not true, the tests are just ignored. We can assume that the tests are run in the EST time zone; if the tests are run somewhere else, they will be ignored automatically. Similarly, we can assume that the third-party code version is higher than the build/version 123; if the build version is lower, the tests will be ignored.
Let's write the code to validate our assumption about Assume
.
Here, we will try to solve the SONAR server issue. We will assume that SONAR is not running. If SONAR runs during the test execution, the assumption will fail and the tests will be ignored.
Create an Assumption
test class. The following is the body of the class:
public class Assumption { boolean isSonarRunning = false; @Test public void very_critical_test() throws Exception { isSonarRunning = true; Assume.assumeFalse(isSonarRunning); assertTrue(true); } }
Here, for simplicity, we added a isSonarRunning
variable to replicate a SONAR server facade. In the actual code, we can call an API to get the value. We will set the variable to false
. Then, in the test, we will reset the value to true
. This means SONAR is running. So, our assumption that SONAR is not running is false; hence, the test will be ignored.
The following screenshot shows that the test is ignored. We didn't annotate the test using @Ignore
:
When we change the value of the isSonarRunning
variable to false,
as given in the following code snippet, the test will be executed:
public void very_critical_test() throws Exception { isSonarRunning = false; Assume.assumeFalse(isSonarRunning); assertTrue(true); }
Continuous integration tools such as Jenkins can run multiple tools such as Sonar to acquire code-quality metrics. It's always a good practice to have a build pipeline where the code quality is only checked after the tests pass. This prevents the CPU-intensive tasks from occurring at the same time.
Assumption is also used in the @Before
methods, but be careful not to overuse it. Assumption is good for use with TDD where one writes pretests ahead of time.
Exploring the test suite
To run multiple test cases, JUnit 4 provides Suite.class
and the @Suite.SuiteClasses
annotation. This annotation takes an array (comma separated) of test classes.
Create a TestSuite
class and annotate the class with @RunWith(Suite.class)
. This annotation will force Eclipse to use the suite runner.
Next, annotate the class with @Suite.SuiteClasses({ AssertTest.class, TestExecutionOrder.class, Assumption.class })
and pass comma-separated test class names.
The following is the code snippet:
import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ AssertTest.class, TestExecutionOrder.class,Assumption.class }) public class TestSuite { }
During execution, the suite will execute all the tests. The following is a screenshot of the suite run. Check whether it runs seven tests out of the three test fixtures: AssertTest
, TestExecutionOrder
, and Assumption
.
A test suite is created for group-related tests such as a group of data access, API usage tests, or a group of input validation logic tests.
Asserting with assertThat
Joe Walnes created the assertThat(Object actual, Matcher matcher)
method. General consensus is that assertThat
is readable and more useful than assertEquals
. The syntax of the assertThat
method is as follows:
public static void assertThat(Object actual, Matcher matcher
Here, Object
is the actual value received and Matcher
is an implementation of the org.hamcrest.Matcher
interface. This interface comes from a separate library called hamcrest.jar
.
A matcher enables a partial or an exact match for an expectation, whereas assertEquals
uses an exact match. Matcher
provides utility methods such as is
, either
, or
, not
, and hasItem
. The Matcher
methods use the builder pattern so that we can combine one or more matchers to build a composite matcher chain. Just like StringBuilder
, it builds a string in multiple steps.
The following are a few examples of matchers and assertThat
:
assertThat(calculatedTax, is(not(thirtyPercent)) );
assertThat(phdStudentList, hasItem(DrJohn) );
assertThat(manchesterUnitedClub, both( is(EPL_Champion)).and(is(UEFA_Champions_League_Champion)) );
The preceding examples are more English than a JUnit test code. So, anyone can understand the intent of the code and test, and a matcher improves readability.
Hamcrest provides a utility matcher class called org.hamcrest.CoreMatchers
.
A few utility methods of CoreMatchers
are allOf
, anyOf
, both
, either
, describedAs
, everyItem
, is
, isA
, anything
, hasItem
, hasItems
, equalTo
, any
, instanceOf
, not
, nullValue
, notNullValue
, sameInstance
, theInstance
,startsWith
, endsWith
, and containsString
. All these methods return a matcher.
We worked with assertEquals
; so, let's start with equalTo
. The equalTo
method is equivalent to assertEquals
.
Comparing matchers – equalTo, is, and not
Create a AssertThatTest.java
JUnit test and static import org.hamcrest.CoreMatchers.*;
as follows:
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import org.junit.Test; public class AssertThatTest { @Test public void verify_Matcher() throws Exception { int age = 30; assertThat(age, equalTo(30)); assertThat(age, is(30)); assertThat(age, not(equalTo(33))); assertThat(age, is(not(33))); } }
Set the age
variable to 30
and then likewise for assertEquals
and call equalTo
, which here is Matcher
. The equalTo
method takes a value. If the Matcher
value doesn't match the actual value, then assertThat
throws an AssertionError
exception.
Set the age
variable value to 29
and rerun the test. The following error will occur:
The is(a)
attribute takes a value and returns a Boolean and behaves similar to equalTo(a)
. The is(a)
attribute is the same as is(equalTo(a))
.
The not
attribute takes a value or a matcher. In the preceding code, we used assertThat(age, is(not(33)));
. This expression is nothing but age is not 33
and is more readable than the assert
methods.
Working with compound value matchers – either, both, anyOf, allOf, and not
In this section, we will use either
, both
, anyOf
, allOf
, and not
. Add the following test to the AssertThatTest.java
file:
@Test public void verify_multiple_values() throws Exception { double marks = 100.00; assertThat(marks, either(is(100.00)).or(is(90.9))); assertThat(marks, both(not(99.99)).and(not(60.00))); assertThat(marks, anyOf(is(100.00),is(1.00),is(55.00),is(88.00),is(67.8))); assertThat(marks, not(anyOf(is(0.00),is(200.00)))); assertThat(marks, not(allOf(is(1.00),is(100.00), is(30.00)))); }
In the preceding example, a marks
double variable is initialized with a value of 100.00
. This variable value is asserted with an either
matcher.
Basically, using either
, we can compare two values against an actual or calculated value. If any of them match, then the assertion is passed. If none of them match, then AssertionError
is thrown.
The either(Matcher)
method takes a matcher and returns a CombinableEitherMatcher
class. The CombinableEitherMatcher
class has a or(Matcher other)
method so that either
and or
can be combined.
The or(Matcher other)
method is translated to return (new CombinableMatcher(first)).or(other);
and finally to new CombinableMatcher(new AnyOf(templatedListWith(other)));
.
Using both
, we can compare two values against an actual or calculated value. If any of them don't match, then the AssertionError
exception is thrown. If both of them match, then the assertion is passed.
A numeric value such as a math score cannot be equal to both 60 and 80. However, we can negate the expression. If the math score is 80, then using the both
matcher we can write the expression as assertThat (mathScore , both (not(60)). and(not (90)))
.
The anyOf
matcher is more like either
with multiple values. Using anyOf
, we can compare multiple values against an actual or calculated value. If any of them match, then the assertion is passed. If none of them match, then the AssertionError
exception is thrown.
The allOf
matcher is more like both
with multiple values. Using allOf
, we can compare multiple values against an actual or calculated value. If any of them don't match, then the AssertionError
exception is thrown. Similar to both
, we can use allOf
along with not
to check whether a value does or doesn't belong to a set.
In the preceding example, using allOf
and not
, we checked whether the marks
attribute is not 1
, 100
, or 30
.
Working with collection matchers – hasItem and hasItems
In the previous section, we asserted a value against multiple values. In this section, we will assert a collection of values against a value or numerous values.
Consider the following example. A salary list is populated with three values: 50.00
, 200.00
, and 500.00
. Use hasItem
to check whether a value exists in a collection, and use hasItems
to check whether multiple values exist in a collection, as shown in the following code:
@Test public void verify_collection_values() throws Exception { List<Double> salary =Arrays.asList(50.0, 200.0, 500.0); assertThat(salary, hasItem(50.00)); assertThat(salary, hasItems(50.00, 200.00)); assertThat(salary, not(hasItem(1.00))); }
The hasItem
matcher has two versions: one takes a value and the other takes a matcher. So, we can check a value in a collection using hasItem
, or check whether a value doesn't exist in a collection using not
and hasItem
. The hasItems
matcher operates on a set of values.
Exploring string matchers – startsWith, endsWith, and containsString
In this section, we will explore the string matchers. CoreMatchers
has three built-in string matcher methods. In the following example, a String
variable name is assigned a value and then we assert that the name starts with a specific value, contains a value, and ends with a value:
@Test public void verify_Strings() throws Exception { String name = "John Jr Dale"; assertThat(name, startsWith("John")); assertThat(name, endsWith("Dale")); assertThat(name, containsString("Jr")); }
The startsWith
matcher operates on string only. It checks whether the string starts with the given string. The endsWith
matcher checks whether the string ends with the given string. The containsString
matcher checks whether the string contains another string.
Sometimes, a method calls to return a JSON response. Using containsString
, a specific value can be asserted.
Note
Note that startsWith
, endsWith
, and containsStrings
are not the only string matchers. Other built-in matchers such as both
, either
, anyOf
, and so on, can be applied to a String
object.
Exploring built-in matchers
JUnitMatchers
has built-in matcher methods, but all of these methods are deprecated. Use Hamcrest matchers instead of using JUnitMatchers
.
Building a custom matcher
We can build our own matchers to use in assertThat
. How about building a matcher
that will compare two values and return true
only if the actual object is less than or equal to the expected value?
Call it a lessThanOrEqual
matcher. It should be allowed to use with any object that can be compared so that we can use an Integer
or Double
or String
type or any custom class that implements the Comparable
interface.
For example, assertThat(100, lessThanOrEqual(200))
should pass, but assertThat(100, lessThanOrEqual(50))
should fail and assertThat("john123", lessThanOrEqual("john123"))
should pass, but assertThat("john123", lessThanOrEqual("john12"))
should fail.
Follow the ensuing steps to build the lessThanOrEqual
matcher:
- Create a
LessThanOrEqual
class under thecom.packtpub.junit.recap
package. - To build a custom matcher, a class should implement the
Matcher
interface. However, Hamcrest recommends extendingorg.hamcrest.BaseMatcher
rather than implementing theMatcher
interface. So, we will extendBaseMatcher
. TheBaseMatcher
class is an abstract class, and it doesn't implementdescribeTo(Description description)
andmatches(Object t)
.The
public boolean matches(Object obj)
method is invoked byassertThat
. If this method returnsfalse
, then anAssertionError
exception is thrown.The
public void describeTo(Description description)
method is invoked whenmatches(Object obj)
returnsfalse
. This method builds the description of an expectation.The following code snippet explains how
assertThat
works:if(!matcher.matches(actual)){ Description description = new StringDescription(); description.appendText(reason).appendText("\nExpected: ).appendDescriptionOf(matcher).appendText("\n but: "); matcher.describeMismatch(actual, description); throw new AssertionError(description.toString()); }
Note that when
matcher.matches()
returnsfalse
, the description is built from the actual value and the matcher. TheappendDescriptionOf()
method calls thedescribeTo()
method of the matcher to build the error message.Finally,
matcher.describeMismatch(actual, description)
appends the stringbut: was <<actual>>
. - The
lessThanOrEqual
class needs to compare two objects, so theMatcher
class should be operated on theComparable
objects. Create a generic class that operates with any type that implements theComparable
interface, as follows:public class LessThanOrEqual<T extends Comparable<T>> extends BaseMatcher<Comparable<T>> { }
- Now we need to implement the
describeTo
andmatches
methods. TheassertThat
method will pass the actual value to the matcher'smatches(Object o)
method, andlessThanOrEqual
will accept a value to compare with the actual. So, in thematches
method, we need two comparable objects: one passed as a parameter and the other passed to a matcher object. The expected value is passed during thematcher
object instantiation as follows:assertThat (actual, matcher(expectedValue)).
We will store the
expectedValue
during theMatcher
object creation and use it in thematches()
method to compare theexpectedValue
with theactual
as follows:public class LessThanOrEqual<T extends Comparable<T>> extends BaseMatcher<Comparable<T>> { private final Comparable<T> expectedValue; public LessThanOrEqual(T expectedValue) { this.expectedValue = expectedValue; } @Override public void describeTo(Description description) { description.appendText(" less than or equal(<=) "+expectedValue); } @Override public boolean matches(Object t) { int compareTo = expectedValue.compareTo((T)t); return compareTo > -1; } }
The preceding
LessThanOrEqual
class should returntrue
only ifexpectedValue.compareTo(actual) >= 0
and then thedescribeTo()
method appends the string"less than or equals (<=) "+ expectedValue
text to thedescription,
so that if the assertion fails, then the "less than or equals (<=) "+ expectedValue
message will be shown. - The
assertThat
method takes a matcher butnew LessThanOrEqual(expectedValue)
doesn't look good. We will create astatic
method in theLessThanOrEqual
class to create a new object ofLessThanOrEqual
. Call this method from theassertThat
method as follows:@Factory public static<T extends Comparable<T>> Matcher<T> lessThanOrEqual(T t) { return new LessThanOrEqual(t); }
The
@Factory
annotation isn't necessary but needed for a Hamcrest tool. When we create many custom matchers, then it becomes annoying to import them all individually. Hamcrest ships with aorg.hamcrest.generator.config.XmlConfigurator
command-line tool that picks up predicates annotated with the@Factory
annotation and collects them in aMatcher
class for easy importing. - Static import the
LessThanOrEqual
class and add a test toAssertThatTest.java
to validate the custom matcher, as shown in the following code:@Test public void lessthanOrEquals_custom_matcher() throws Exception { int actualGoalScored = 2; assertThat(actualGoalScored, lessThanOrEqual(4)); assertThat(actualGoalScored, lessThanOrEqual(2)); double originalPI = 3.14; assertThat(originalPI, lessThanOrEqual(9.00)); String authorName = "Sujoy"; assertThat(authorName, lessThanOrEqual("Zachary")); }
This test should pass.
- How about testing the code with a greater value? In Java,
Integer.MAX_VALUE
holds the maximum integer value andInteger.MIN_VALUE
holds the minimum integer value. If we expect that the maximum value will be greater than or equal to the minimum value, then the assertion should fail. Consider the following code snippet:int maxInt = Integer.MAX_VALUE; assertThat(maxInt, lessThanOrEqual(Integer.MIN_VALUE));
This will throw the following error:
Creating parameterized tests
Parameterized tests are used for multiple iterations over a single input to stress the object in test. The primary reason is to reduce the amount of test code.
In TDD, the code is written to satisfy a failing test. The production code logic is built from a set of test cases and different input values. For example, if we need to build a class that will return the factorial of a number, then we will pass different sets of data and verify that our implementation passes the validation.
We know that the factorial of 0 is 1, the factorial of 1 is 1, the factorial of 2 is 2, the factorial of 3 is 6, the factorial of 4 is 24, and so on.
So, if we write tests such as factorial_of_1_is_1
and factorial_of_4_is_24
, then the test class will be polluted very easily. How many methods will we write?
We can create two arrays: one with the expected values and the other with the original numbers. Then, we can loop through the arrays and assert the result. We don't have to do this because the JUnit 4 framework provides us with a similar solution. It gives us a Parameterized
runner.
We read about the @RunWith
annotation in the preceding section. Parameterized
is a special type of runner and can be used with the @RunWith
annotation.
Parameterized comes with two flavors: constructor and method.
Working with parameterized constructors
Perform the following steps to build a parameterized test with a constructor:
- Create a source folder
src
and add aFactorial.java
class undersrc/ com.packtpub.junit.recap
. - Implement the factorial algorithm. Add the following code to the
Factorial.java
class:package com.packtpub.junit.recap; public class Factorial { public long factorial(long number) { if(number == 0) { return 1; } return number*factorial(number-1); } }
- Add a
ParameterizedFactorialTest.java
test undertest/ com.packtpub.junit.recap
and annotate the class with@RunWith(Parameterized.class)
as follows:import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @RunWith(Parameterized.class) public class ParameterizedFactorialTest { }
- Add a method to create a dataset for factorial algorithm. The method should return
Collection
of theObject[]
method. We need a collection of two dimensional arrays to hold the numbers and factorial values. To define the data parameters, annotate the method with@Parameters
.The following code snippet defines a
@parameters
methodfactorialData()
:@Parameters public static Collection<Object[]> factorialData() { return Arrays.asList(new Object[][] { { 0, 1 }, { 1, 1 }, { 2, 2 }, { 3, 6 }, { 4, 24 }, { 5, 120 },{ 6, 720 } }); }
Check whether the arrays hold the number and the expected factorial result (0's factorial is 1, 5's factorial is 120, and so on).
- The
Parameterized
runner needs a constructor to pass the collection of data. For each row in the collection, the 0th array element will be passed as the 1st constructor argument, the next index will be passed as 2nd argument, and so on, as follows:private int number; private int expectedResult; public ParameterizedFactorialTest(int input, int expected) { number= input; expectedResult= expected; }
In the test class, we added two members to hold the number and the expected factorial value. In the constructor, set these values. The
Parameterized
runner will loop through the data collection (annotated with a@Parameters
annotation) and pass the values to the constructor.For example, it will pass 0 as input and 1 as expected, then 1 as input and 1 as expected, and so on.
- Now, we need to add a test method to assert the number and the factorial as follows:
@Test public void factorial() throws Exception { Factorial fact = new Factorial(); assertEquals(fact.factorial(number),expectedResult); }
We created a
Factorial
object and passed the number to get the actual result and then asserted the actual value withexpectedResult
. Here, the runner will create seven instances of the test class and execute the test method.The following screenshot shows the result of the test run taken from Eclipse:
Note that the seven tests run and the tests names are [0] factorial[0], [1] factorial[1], and so on till [6].
Note
If the dataset returns an empty collection, the test doesn't fail; actually, nothing happens.
If the number of parameters in the object array and the constructor argument don't match, then a
java.lang.IllegalArgumentException: wrong number of arguments
exception is thrown. For example, { 0, 1, 3 } will throw an exception as 3 arguments are passed, but constructor can accept only 2.If the constructor is not defined but the data set contains a value, then the
java.lang.IllegalArgumentException: wrong number of arguments
exception is thrown.
Working with parameterized methods
We learned about the parameterized constructor; now we will run the parameterized test excluding the constructor. Follow the ensuing steps to run the test using the @Parameter
annotation:
- Add a
ParameterizeParamFactorialTest.java
test class. - Copy the content from the constructor test and delete the constructor. Change the class members to public, as follows:
@RunWith(Parameterized.class) public class ParameterizeParamFactorialTest { @Parameters public static Collection<Object[]> factorialData() { return Arrays.asList(new Object[][] { { 0, 1 }, { 1, 1 }, { 2, 2 }, { 3, 6 }, { 4, 24 }, { 5, 120 },{ 6, 720 } }); } public int number; public int expectedResult; @Test public void factorial() throws Exception { Factorial fact = new Factorial(); assertEquals(fact.factorial(number),expectedResult); } }
- If we run the test, it will fail as the reflection process won't find the matching constructor. JUnit provides an annotation to loop through the dataset and set the values to the class members.
@Parameter(value=index)
takes a value. The value is the array index of the data collection object array. Make sure that thenumber
andexpectedResult
variables arepublic
; otherwise, the security exception will be thrown. Annotate them with the following parameters:@Parameter(value=0) public int number; @Parameter(value=1) public int expectedResult;
Here, for each row in the data collection, the
number
variable will hold the 0th index of the array and theexpectedResult
variable will hold the 1st index. - Run the test; seven tests will be executed.
Giving a name
In the constructor example, we found that the test names are assigned with indexes such as [0], [1], and so on. So, if a test fails, then it is not easy to identify the data. To identify an individual test case in a parameterized test, a name is required. The @Parameters
annotation allows placeholders that are replaced at runtime, and we can use them. The following are the placeholders:
{index}
: This represents the current parameter index{0}, {1},…
: This represents the first, second, and so on, parameter values
The following code snippet annotates the dataset with the name placeholders:
@Parameters(name = "{index}: factorial({0})={1}") public static Collection<Object[]> factorialData() { return Arrays.asList(new Object[][] { { 0, 1 }, { 1, 1 }, { 2, 2 }, { 3, 6 }, { 4, 24 }, { 5, 120 },{ 6, 720 } }); }
Eclipse has a bug that chops off the name.
Working with timeouts
JUnit tests are automated to get quick feedback after a change in the code. If a test runs for a long time, it violates the quick feedback principle. JUnit provides a timeout value (in milliseconds) in the @Test
annotation to make sure that if a test runs longer than the specified value, the test fails.
The following is an example of a timeout:
@Test(timeout=10) public void forEver() throws Exception { Thread.sleep(100000); }
Here, the test will fail automatically after 10 milliseconds. The following is an Eclipse screenshot that shows the error:
Exploring JUnit theories
A theory is a kind of a JUnit test but different from the typical example-based JUnit tests, where we assert a specific data set and expect a specific outcome. JUnit theories are an alternative to JUnit's parameterized tests. A JUnit theory encapsulates the tester's understanding of an object's universal behavior. This means whatever a theory asserts is expected to be true for all data sets. Theories are useful for finding bugs in boundary-value cases.
Parameterized tests allow us to write flexible data-driven tests and separate data from the test methods. Theories are similar to parameterized tests—both allow us to specify the test data outside of the test case.
Parameterized tests are good but they have the following drawbacks:
- Parameters are declared as member variables. They pollute the test class and unnecessarily make the system complex.
- Parameters need to be passed to the single constructor or variables need to be annotated, simply making the class incomprehensible.
- Test data cannot be externalized.
Theory comes up with many annotations and a runner class. Let's examine the important annotations and classes in theory, as follows:
@Theory
: Like@Test
, this annotation identifies a theory test to run. The@Test
annotation doesn't work with a theory runner.@DataPoint
: This annotation identifies a single set of test data (similar to@Parameters
), that is, either a static variable or a method.@DataPoints
: This annotation identifies multiple sets of test data, generally an array.@ParametersSuppliedBy
: This annotation provides the parameters to the test cases.Theories
: This annotation is a JUnit runner for the theory-based test cases and extendsorg.junit.runners.BlockJUnit4ClassRunner
.ParameterSupplier
: This is an abstract class that gives us the handle on the parameters that we can supply to the test case.
We will start with a simple theory and then explore more. Perform the following steps:
- Create a
MyTheoryTest.java
class and annotate the class with@RunWith(Theories.class)
. To run a theory, this special runner is required. Consider the following code:@RunWith(Theories.class) public class MyTheoryTest { }
- Now run the test. It will fail with the
java.lang.Exception: No runnable methods
error because no theory is defined yet. Like the@Test
annotation, we will define a method and annotate it with@Theory
as follows:@RunWith(Theories.class) public class MyTheoryTest { @Theory public void sanity() { System.out.println("Sanity check"); } }
Run the theory, and it will be executed with no error. So, our theory setup is ready.
- Define a
public static
String
with aname
variable and annotate this variable with@DataPoint
. Now execute the test, nothing special happens. If a theory method (annotated with@Theory
) takes an argument and a variable annotated with@DataPoint
matches the type, then the variable is passed to the theory during execution. So, change thesanity
method and add aString
argument to pass@DataPoint to the sanity()
method, as follows:@RunWith(Theories.class) public class MyTheoryTest { @DataPoint public static String name ="Jack"; @Theory public void sanity(String aName) { System.out.println("Sanity check "+aName); } }
Now run the theory. It will pass the
@DataPoint
name to thesanity(String aName)
method during execution and the name will be printed to the console. - Now, add another static
@DataPoint
, call itmike
, and rename thename
variable tojack
, as follows:@RunWith(Theories.class) public class MyTheoryTest { @DataPoint public static String jack ="Jack"; @DataPoint public static String mike ="Mike"; @Theory public void sanity(String aName) { System.out.println("Sanity check "+aName); } }
During theory execution, both the
@DataPoint
variables will be passed to thesanity(String aName)
method. The output will be as follows: - Now, slightly modify the
sanity()
method—rename theaName
argument tofirstName
and add a secondString
argument,lastName
. So now thesanity
method takes theString
arguments,fistName
andlastName
. Print these variables using the following code:@RunWith(Theories.class) public class MyTheoryTest { @DataPoint public static String jack ="Jack"; @DataPoint public static String mike ="Mike"; @Theory public void sanity(String firstName, String lastName) { System.out.println("Sanity check "+firstName+", "+lastName); } }
When executed, the output will be as follows:
So, 2 x 2 = 4 combinations are used. When the multiple
@DataPoint
annotations are defined in a test, the theories apply to all possible well-typed combinations of data points for the test arguments. - So far we have only examined single-dimension variables. The
@DataPoints
annotation is used to provide a set of data. Add a staticchar
array to hold the character variables and add aTheory
method to accept two characters. It will execute the theory with 9 (3 ^ 2) possible combinations as follows:@DataPoints public static char[] chars = new char[] {'A', 'B', 'C'}; @Theory public void build(char c, char d) { System.out.println(c+" "+d); }
The following is the output:
Externalizing data using @ParametersSuppliedBy and ParameterSupplier
So far, we have covered how to set up test data using @DataPoint
and @DataPoints
. Now, we will use external classes to supply data in our tests using @ParametersSuppliedBy
and ParameterSupplier
. To do this, perform the following steps:
- Create an
Adder.java
class. This class will have two overloadedadd()
methods to add numbers and strings. We will unit test the methods using theory.The following is the
Adder
class:public class Adder { public Object add(Number a, Number b) { return a.doubleValue()+b.doubleValue(); } public Object add(String a, String b) { return a+b; } }
- Create an
ExternalTheoryTest.java
theory as follows:@RunWith(Theories.class) public class ExternalTheoryTest { }
- We will not use
@DataPoints
to create data. Instead, we will create a separate class to supply numbers to validate theadd
operation. JUnit provides aParameterSupplier
class for this purpose.ParameterSupplier
is an abstract class, and it forces you to define a method as follows:public abstract List<PotentialAssignment> getValueSources(ParameterSignature parametersignature);
PotentialAssignment
is an abstract class that JUnit theories use to provide test data to test methods in a consistent manner. It has a staticforValue
method that you can use to get an instance ofPotentialAssignment
.Create a
NumberSupplier
class to supply different types of numbers:float
,int
,double
,long
, and so on. Extend theParameterSupplier
class as follows:import org.junit.experimental.theories.ParameterSignature; import org.junit.experimental.theories.ParameterSupplier; import org.junit.experimental.theories.PotentialAssignment; public class NumberSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) { List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); list.add(PotentialAssignment.forValue("long", 2L)); list.add(PotentialAssignment.forValue("float", 5.00f)); list.add(PotentialAssignment.forValue("double", 89d)); return list; } };
Check whether the overridden method creates a list of
PotentialAssignment
values of different numbers. - Now, modify the theory to add two numbers. Add a theory method as follows:
import org.junit.experimental.theories.ParametersSuppliedBy; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; @RunWith(Theories.class) public class ExternalTheoryTest { @Theory public void adds_numbers( @ParametersSuppliedBy(NumberSupplier.class) Number num1, @ParametersSuppliedBy(NumberSupplier.class) Number num2) { System.out.println(num1 + " and " + num2); } }
Check the
adds_numbers
method; twoNumber
argumentsnum1
andnum2
are annotated with@ParametersSuppliedBy(NumberSupplier.class)
.When this theory is executed, the
NumberSupplier
class will pass a list. - Execute the theory; it will print the following result:
- Now, we can check our
Adder
functionality. Modify the theory to assert the result.Create an instance of the
Adder
class and call theadd
method by passingnum1
andnum2
. Add the two numbers andassert
the value with the results ofAdder
.The
assertEquals(double, double)
method is deprecated as the double value calculation results in an unpredictable result. So, theassert
class adds another version ofassertEquals
fordoubles
; it takes three arguments: actual, expected, and a delta. If the difference between theactual
and theexpected
value is greater than or equal to delta, then the assertion passes as follows:@RunWith(Theories.class) public class ExternalTheoryTest { @Theory public void adds_numbers( @ParametersSuppliedBy(NumberSupplier.class) Number num1, @ParametersSuppliedBy(NumberSupplier.class) Number num2) { Adder anAdder = new Adder(); double expectedSum = num1.doubleValue()+num2.doubleValue(); double actualResult = (Double)anAdder.add(num1, num2); assertEquals(actualResult, expectedSum, 0.01); } }
The
Adder
class has anadd
method forString
. Create aStringSupplier
class to supplyString
values to our theory and modify the theory class to verify theadd (String, String)
method behavior. You can assert theStrings
as follows:String expected = str1+str2;
assertEquals(expected, actual);
Here,
str1
andstr2
are the two method arguments of the theory.
Dealing with JUnit rules
Rules allow very flexible addition or redefinition of the behavior of each test method in a test class. Rules are like Aspect Oriented Programming (AOP); we can do useful things before and/or after the actual test execution. You can find more information about AOP at http://en.wikipedia.org/wiki/Aspect-oriented_programming.
We can use the inbuilt rules or define our custom rule.
In this section, we will look at the inbuilt rules and create our custom Verifier and WatchMan rule.
Playing with the timeout rule
The timeout rule applies the same timeout to all the test methods in a class. Earlier, we used the timeout in the @Test
annotation as follows:
@Test(timeout=10)
The following is the syntax of the timeout rule:
import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; public class TimeoutTest { @Rule public Timeout globalTimeout = new Timeout(20); @Test public void testInfiniteLoop1() throws InterruptedException{ Thread.sleep(30); } @Test public void testInfiniteLoop2() throws InterruptedException{ Thread.sleep(30); } }
When we run this test, it times out after 20 milliseconds. Note that the timeout is applied globally to all methods.
Working with the ExpectedException rule
The ExpectedException
rule is an important rule for handling exceptions. It allows you to assert the expected exception type and the exception message, for example, your code may throw a generic exception (such as IllegalStateException
) for all failure conditions, but you can assert the generic exception message to verify the exact cause.
Earlier, we used @Test(expected=Exception class)
to test the error conditions.
The ExpectedException
rule allows in-test specification of expected exception types and messages.
The following code snippet explains how an exception rule can be used to verify the exception class and the exception message:
public class ExpectedExceptionRuleTest { @Rule public ExpectedException thrown= ExpectedException.none(); @Test public void throwsNothing() { } @Test public void throwsNullPointerException() { thrown.expect(NullPointerException.class); throw new NullPointerException(); } @Test public void throwsIllegalStateExceptionWithMessage() { thrown.expect(IllegalStateException.class); thrown.expectMessage("Is this a legal state?"); throw new IllegalStateException("Is this a legal state?"); } }
The expect
object sets the expected exception class and expectMessage
sets the expected message in the exception. If the message or exception class doesn't match the rule's expectation, the test fails. The ExpectedException
object thrown is reset on each test.
Unfolding the TemporaryFolder rule
The TemporaryFolder
rule allows the creation of files and folders that are guaranteed to be deleted when the test method finishes (whether it passes or fails). Consider the following code:
@Rule public TemporaryFolder folder = new TemporaryFolder(); @Test public void testUsingTempFolder() throws IOException { File createdFile = folder.newFile("myfile.txt"); File createdFolder = folder.newFolder("mysubfolder"); }
Exploring the ErrorCollector rule
The ErrorCollector
rule allows the execution of a test to continue after the first problem is found (for example, to collect all the incorrect rows in a table and report them all at once) as follows:
import org.junit.rules.ErrorCollector; import static org.hamcrest.CoreMatchers.equalTo; public class ErrorCollectorTest { @Rule public ErrorCollector collector = new ErrorCollector(); @Test public void fails_after_execution() { collector.checkThat("a", equalTo("b")); collector.checkThat(1, equalTo(2)); collector.checkThat("ae", equalTo("g")); } }
In this example, none of the verification passes but the test still finishes its execution, and at the end, notifies all errors.
The following is the log—the arrows indicate the errors—and also note that only one test method is being executed but Eclipse indicates three failures:
Working with the Verifier rule
Verifier is a base class of ErrorCollector
, which can otherwise turn passing tests into failing tests if a verification check fails. The following example demonstrates the Verifier
rule:
public class VerifierRuleTest { private String errorMsg = null; @Rule public TestRule rule = new Verifier() { protected void verify() { assertNull("ErrorMsg should be null after each test execution",errorMsg); } }; @Test public void testName() throws Exception { errorMsg = "Giving a value"; } }
Verifier's verify
method is executed after each test execution. If the verify
method defines any assertions, and that assertion fails, then the test is marked as failed.
In the preceding example, the test should not fail as the test method doesn't perform any comparison; however, it still fails. It fails because the Verifier rule checks that after every test execution, the errorMsg
string should be set as null, but the test method sets the value to Giving a value
; hence, the verification fails.
Learning the TestWatcher rule
TestWatcher
(and the deprecated TestWatchman
) are base classes for rules that take note of the testing action, without modifying it. Consider the following code:
@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestWatcherTest { private static String dog = ""; @Rule public TestWatcher watchman = new TestWatcher() { @Override public Statement apply(Statement base, Description description) { return super.apply(base, description); } @Override protected void succeeded(Description description) { dog += description.getDisplayName() + " " + "success!\n"; } @Override protected void failed(Throwable e, Description description) { dog += description.getDisplayName() + " " + e.getClass().getSimpleName() + "\n"; } @Override protected void starting(Description description) { super.starting(description); } @Override protected void finished(Description description) { super.finished(description); } }; @Test public void red_test() { fail(); } @Test public void green() { } @AfterClass public static void afterClass() { System.out.println(dog); } }
We created a TestWatcher
class to listen to every test execution, collected the failure, and success instances, and at the end, printed the result in the afterClass()
method.
The following is the error shown on the console:
green(com.packtpub.junit.recap.rule.TestWatcherTest) success! red_test(com.packtpub.junit.recap.rule.TestWatcherTest) AssertionError
Working with the TestName rule
The TestName
rule makes the current test name available inside test methods. The TestName
rule can be used in conjunction with the TestWatcher
rule to make a unit testing framework compile a unit testing report.
The following test snippet shows that the test name is asserted inside the test:
public class TestNameRuleTest { @Rule public TestName name = new TestName(); @Test public void testA() { assertEquals("testA", name.getMethodName()); } @Test public void testB() { assertEquals("testB", name.getMethodName()); } }
The following section uses the TestName
rule to get the method name before test execution.
Handling external resources
Sometimes JUnit tests need to communicate with external resources such as files or databases or server sockets. Dealing with external resources is always messy because you need to set up state and tear it down later. The ExternalResource
rule provides a mechanism that makes resource handling a bit more convenient.
Previously, when you had to create files in a test case or work with server sockets, you had to set up a temporary directory, or open a socket in a @Before
method and later delete the file or close the server in an @After
method. But now, JUnit provides a simple AOP-like mechanism called the ExternalResource
rule that makes this setup and cleanup work the responsibility of the resource.
The following example demonstrates the ExternalResource
capabilities. The Resource
class represents an external resource and prints the output in the console:
class Resource{ public void open() { System.out.println("Opened"); } public void close() { System.out.println("Closed"); } public double get() { return Math.random(); } }
The following test class creates ExternalResource
and handles the resource lifecycle:
public class ExternalResourceTest { Resource resource; public @Rule TestName name = new TestName(); public @Rule ExternalResource rule = new ExternalResource() { @Override protected void before() throws Throwable { resource = new Resource(); resource.open(); System.out.println(name.getMethodName()); } @Override protected void after() { resource.close(); System.out.println("\n"); } }; @Test public void someTest() throws Exception { System.out.println(resource.get()); } @Test public void someTest2() throws Exception { System.out.println(resource.get()); } }
The anonymous ExternalResource
class overrides the before
and after
methods of the ExternalResource
class. In the before
method, it starts the resource and prints the test method name using the TestName
rule. In the after
method, it just closes the resource.
The following is the test run output:
Opened someTest2 0.5872875884671511 Closed Opened someTest 0.395586457988541 Closed
Note that the resource is opened before test execution and closed after the test. The test name is printed using the TestName
rule.
Exploring JUnit categories
The Categories
runner runs only the classes and methods that are annotated with either the category given with the @IncludeCategory
annotation or a subtype of that category. Either classes or interfaces can be used as categories. Subtyping works, so if you use @IncludeCategory(SuperClass.class)
, a test marked @Category({SubClass.class})
will be run.
We can exclude categories by using the @ExcludeCategory
annotation.
We can define two interfaces using the following code:
public interface SmartTests { /* category marker */ } public interface CrazyTests { /* category marker */ } public class SomeTest { @Test public void a() { fail(); } @Category(CrazyTests.class) @Test public void b() { } } @Category({CrazyTests.class, SmartTests.class}) public class OtherTest { @Test public void c() { } } @RunWith(Categories.class) @IncludeCategory(CrazyTests.class) @SuiteClasses( { SomeTest.class, OtherTest.class }) // Note that Categories is a kind of Suite public class CrazyTestSuite { // Will run SomeTest.b and OtherTest.c, but not SomeTest.a } @RunWith(Categories.class) @IncludeCategory(CrazyTests.class) @ExcludeCategory(SmartTests.class) @SuiteClasses( { SomeTest.class, OtherTest.class }) public class CrazyTestSuite { // Will run SomeTest.b, but not SomeTest.a or OtherTest.c }