I have seen several examples in the past of using parameterization with JUnit 4, but always felt like I didn't completely grasp what was going on. So if I don't understand something, I should try it out right?
My example class to test with parameterization will be a nice car with one state-changing method that will add a number of miles to the total traveled, whose value will be stored in a odometer property:
package prystasj.junit
class Lambo {
int odometer
def drive(miles) {
if (miles > 0) odometer += miles
}
@Override String toString() {
"${getClass().getName()}[odometer=$odometer]"
}
}
To test the odometer, I will "drive" distances of 5, 10, then 20 miles, checking the odometer along the way. At the end, my low-use Lambo should have traveled a total of 35 miles. To do so, I will use the same Lambo instance throughout the tests.
The @BeforeClass annotation will run the annotated static method once per test run, so I will initialize my Lambo, which also must be static, there:
static Lambo lambo
@BeforeClass static void createLambo() {
lambo = new Lambo()
println "Created Lambo: $lambo"
}
For every run of my test method, I want to receive a number miles to drive and the expected value of the odometer afterwards. Therefore, I need a way to set these values in my test class via a constructor:
def total
def miles
LamboMileageTest(expectedTotal, milesToDrive) {
this.total = expectedTotal
this.miles = milesToDrive
}
The test parameters come from a static data method that returns a collection of arrays. Each entry in the collection will have its values passed as parameters to my constructor above:
@Parameters static def data() {
def data = []
def expectedTotal = 0
[5, 10, 20].each { miles ->
expectedTotal += miles
def group = [miles, expectedTotal] as Integer[]
data << group
}
data
}
Without the conversion via as Integer[], the following exception was thrown:
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.038 sec <<< FAILURE!
initializationError0(prystasj.junit.LamboMileageTest) Time elapsed: 0.007 sec <<< ERROR!
java.lang.Exception: prystasj.junit.LamboMileageTest.data() must return a Collection of arrays.
Additionally, if I tried to append to
data in one statement without the intermediate
group variable, a
GroovyCastException was thrown. I could wrap the right side of the append with parentheses, but I think this way is easier to read.
Each parameter grouping will be passed to each test method in my test class. Here I will have only one test method, but if I had two, each would be run with [5, 5], and then each would be run with [10, 15], and so on.
The test method:
@Test void driveSomeDistance() {
println "Parmeters: [miles=$miles, total=$total]"
lambo.drive(miles)
assertEquals "Odomoter reads '$total'", total, lambo.odometer
}
The output from a successful test run:
Running prystasj.junit.LamboMileageTest
Created Lambo: prystasj.junit.Lambo[odometer=0]
Parmeters: [miles=5, total=5]
Parmeters: [miles=10, total=15]
Parmeters: [miles=20, total=35]
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.173 sec
The entire test class:
package prystasj.junit
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Parameterized.Parameters
import static org.junit.Assert.assertEquals
@RunWith(value=Parameterized)
public class LamboMileageTest {
static Lambo lambo
def total
def miles
LamboMileageTest(milesToDrive, expectedTotal) {
this.miles = milesToDrive
this.total = expectedTotal
}
@BeforeClass static void createLambo() {
lambo = new Lambo()
println "Created Lambo: $lambo"
}
@Parameters static def data() {
def data = []
def expectedTotal = 0
[5, 10, 20].each { miles ->
expectedTotal += miles
def group = [miles, expectedTotal] as Integer[]
data << group
}
data
}
@Test void driveSomeDistance() {
println "Parmeters: [miles=$miles, total=$total]"
lambo.drive(miles)
assertEquals "Odomoter reads '$total'", total, lambo.odometer
}
}
Looking at the nature of a parameterized test class, and at the goal of my tests, I would recommend that this test class only be used for testing the accumulation of miles. If I wanted to test other Lambo functionality or usage, such as a Lambo not being able to drive negative miles, I would create another test class.
Without making use of parameterization, I would of wrote a test class like this:
public class AlternateLamboMileageTest {
Lambo lambo
@Before void setUp() {
lambo = new Lambo()
}
@Test void starting_with_zero_miles_drive_5() {
lambo.odometer = 0
lambo.drive(5)
assertEquals 5, lambo.odometer
}
@Test void starting_with_five_miles_drive_10() {
lambo.odometer = 5
lambo.drive(10)
assertEquals 15, lambo.odometer
}
@Test void starting_with_fifteen_miles_drive_20() {
lambo.odometer = 15
lambo.drive(20)
assertEquals 35, lambo.odometer
}
}
This test class might be easier to follow though, or at least would of been before I started writing. With this approach however, I need to set the odometer before testing the result of a drive as I can't guarantee the order in which the tests are run. If I could, I could also use a static Lambo instance as before and avoid the odometer settings, or JExample to define test dependencies (which I wrote a bit about here).
This new test class also helps expose a design issue with my Lambo modeling, as unless I was some sort of shady used car salesman, I shouldn't be able to set the odometer once its initialized.
Then again, maybe if I was a shady salesman, I could find my way into a real Lambo... :)