Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 4.0

Popper extends JUnit to allow you to specify theories. Theories are assertions about your code's behavior that may be true over (potentially) infinite sets of input values. You might find it useful to pose theories about your Groovy code too.

Example

Let's consider how we might test the following class (example taken from the Popper web site):

Code Block
class Dollar {
    int amount
    Dollar(int amount) { this.amount = amount }
    Dollar times(int value) { amount *= value; return this }
    Dollar divideBy(int value) { amount /= value; return this }
}

With traditional JUnit code, we might test it as follows:

Code Block
import org.junit.Test
import org.junit.runner.JUnitCore

class StandardTest {
    @Test void multiplyThenDivide() {
        assert new Dollar(10).times(6).divideBy(6).amount == 10
    }
}

JUnitCore.main('StandardTest')

This tests the method for one amount value and one m value. Next steps might be to triangulate so that additional values are also tested. In general though, it might be difficult to know when you have done enough values (when to stop) and also what invariants of your class may hold if you simply keep adding more tests without sufficient refactoring. With these factors in mind, Popper provides facilities to make invariants and preconditions of your classes obvious as well as providing an extensible framework for adding new test values.

Here is how you might use Popper to test the above class. First, we have avoided using Hamcrest style assertions in our Groovy code. Groovy's built-in assert method usually allows such assertions to be expressed very elegantly without any additional framework. We'll create a small helper class to allow Groovy-style assertions to be used for method pre-conditions:

Code Block
import static net.saff.theories.assertion.api.Requirements.*
import net.saff.theories.assertion.api.InvalidTheoryParameterException
import net.saff.theories.runner.api.TheoryContainer

class GroovyTheoryContainer extends TheoryContainer {
    def assume(condition) {
        try {
            assert condition
        } catch (AssertionError ae) {
            throw new InvalidTheoryParameterException(condition, is(condition))
        }
    }
    def assumeMayFailForIllegalArguments(Closure c) {
        try {
            c.call()
        } catch (IllegalArgumentException e) {
            throw new InvalidTheoryParameterException(e, isNull())
        }
    }
}

Now, our test becomes:

Code Block
import org.junit.*
import org.junit.runner.*
import net.saff.theories.methods.api.Theory
import net.saff.theories.runner.api.*

@RunWith(Theories)
class PopperTest extends GroovyTheoryContainer {
    private log = [] // for explanatory purposes only
    public static int VAL1 = 0
    public static int VAL2 = 1
    public static int VAL3 = 2
    public static int VAL4 = 5

    @Theory void multiplyIsInverseOfDivide(int amount, int m) {
        assume m != 0
        assert new Dollar(amount).times(m).divideBy(m).amount == amount
        log << [amount, m]
    }

    @After void dumpLog() {
        println log
    }
}

JUnitCore.main('PopperTest')

We have added an additional log variable to this example to explain how Popper works. By default, Popper will use any public fields in our test as test data values VAL1 through VAL4 in our example. It will determine all combinations of the available variables and call the multiplyIsInverseOfDivide() for each combination. This is a very crude way to select test instance values but works for simple tests like this one.

You should also note the assume statement. In our example, we haven't catered for m being 0 which would result in a divide by zero error. The assume statement allows this method precondition to be made explicit. When Popper calls the test method, it will silently ignore any test data combinations which fail the method preconditions. This keeps the preconditions obvious and simplifies creating test data sets.

Here is the output from running this test:

Code Block
JUnit version 4.3.1
.[[0, 1], [0, 2], [0, 5], [1, 1], [1, 2], [1, 5], [2, 1], [2, 2], [2, 5], [5, 1], [5, 2], [5, 5]]

Time: 0.297

OK (1 test)

We wouldn't normally recommend sending this kind of information to standard out when running your test, but here it is very illustrative. Note that all four test values have been used for the amount variable but only three values have been used for m. This is exactly what we want here.

Popper supports an extensible framework for specifying more elaborate algorithms for selecting test data. Instead of the public variables, we can define our own parameter supplier. Here is one which supplies data between a first value and a last value. First the annotation definition (coded in Java):

Code Block
// Java
import net.saff.theories.methods.api.ParametersSuppliedBy;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between {
    int first();
    int last();
}

And the backing supplier (coded in Groovy):

Code Block
import net.saff.theories.methods.api.*
import java.util.*

public class BetweenSupplier extends ParameterSupplier {
    public List getValues(test, ParameterSignature sig) {
        def annotation = sig.supplierAnnotation
        annotation.first()..annotation.last()
    }
}

Now our Groovy test example could become:

Code Block
import org.junit.*
import org.junit.runner.*
import net.saff.theories.methods.api.Theory
import net.saff.theories.runner.api.*

@RunWith(Theories)
class PopperBetweenTest extends GroovyTheoryContainer {
    private int test, total // for explanatory purposes only

    @Theory void multiplyIsInverseOfDivide(
            @Between(first = -4, last = 2) int amount,
            @Between(first = -2, last = 5) int m
    ) {
        total++
        assume m != 0
        assert new Dollar(amount).times(m).divideBy(m).amount == amount
        test++
    }

    @After void dumpLog() {
        println "$test tests performed out of $total combinations"
    }
}

JUnitCore.main('PopperBetweenTest')

When run, this yields:

Code Block
none
none
JUnit version 4.3.1
.49 tests performed out of 56 combinations

Time: 0.234

OK (1 test)

The supplied test values for the test method are (-4, -2), (-4, -1), (-4, 0), ..., (2, 5). The data where m is equal to 0 will be skipped as soon as the assume statement is reached.

Bowling Example

We can also Groovy to make the bowling example a little more succinct:

Code Block
import net.saff.theories.methods.api.*
import net.saff.theories.runner.api.*
import org.junit.runner.*

@RunWith(Theories.class)
class BowlingTests extends GroovyTheoryContainer {
    public static Game STARTING_GAME = new Game()
    public static Game NULL_GAME = null

    public static Bowl THREE = new Bowl(3)
    public static Bowl FOUR = new Bowl(4)
    public static Bowl NULL_BOWL = null
    @DataPoint public Bowl oneHundredBowl() { new Bowl(100) }

    public static int ONE_HUNDRED = 100
    public static int ZERO = 0

    @Theory
    public void shouldBeTenFramesWithTwoRollsInEach(Game game, Bowl first, Bowl second) {
        assume game && first && second
        assume game.isAtBeginning()
        assume !first.isStrike()
        assume !second.completesSpareAfter(first)
        10.times {
            game.bowl(first)
            game.bowl(second)
        }
        assert game.isGameOver()
    }

    @Theory
    public void maximumPinCountIsTen(Bowl bowl) {
        assume bowl
        assert bowl.pinCount() <= 10
    }

    @Theory
    public void pinCountMatchesConstructorParameter(int pinCount) {
        assumeMayFailForIllegalArguments {
            assert new Bowl(pinCount).pinCount() == pinCount
        }
    }
}

JUnitCore.main('BowlingTests')