Test your code

Junit Rules

While this can be read in many ways, this is strictly referring to the @Rule annotation and TestRule interface that are  available for use in Junit. These are not widely used but they do help us in setting up tests and making the code more elegant and less verbose.

What are they good for?

Well they are good for replacing all the @Before and @After logic that needs to serve multiple tests methods. They are doing a great job in integration testing where you need some prerequisites before running the tests (eg like starting and stopping a server, creating and deleting a file). Also when you need to create an framework/api you need to add test support for others that will use it and rules are an efficient way of doing this(for example mock some of your api). They are similar in concept with AOP(aspects), something can happen before/after a test is executed. The rule can be applied easily to multiple tests classes by injecting it using the @Rule annotation.

I never needed these rules.  I could write tests just fine without these. Why should I start using them?

That’s true. But the code is more elegant when using them. Let me give you a quick example. Let us simulate an auction.

We have the item to sell.

public class Item {

    private int reservePrice;
    private String code;

    public Item(String code, int reservePrice) {
        this.code = code;
        this.reservePrice = reservePrice;
    }

    public int getReservePrice() {
        return reservePrice;
    }

    public void setReservePrice(int reservePrice) {
        this.reservePrice = reservePrice;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }
}

The bid.

public class Bid {

    private Item item;
    private int amount;

    public Bid(Item item, int amount) {
        this.item = item;
        this.amount = amount;
    }

    public Item getItem() {
        return item;
    }

    public void setItem(Item item) {
        this.item = item;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }
}

The auction.

public class Auction {

    private List bids;
    private List items;

    public void sendBidForItem(Item item, int amount) {
        Bid bid = new Bid(item, amount);
        bids = Optional.ofNullable(bids).orElse(new ArrayList<>());
        bids.add(bid);
    }

    public List getAllBidsForItem(final Item item) {
        return bids.stream().filter(bid -> bid.getItem().getCode().equals(item.getCode())).collect(Collectors.toList());
    }

    public Bid sellItem(Item item) {
        Bid highestBid = bids.stream()
                .filter(bid -> bid.getItem().getCode().equals(item.getCode()))
                .max(Comparator.comparing(bid -> bid.getAmount()))
                .get();
        if (highestBid.getAmount() < item.getReservePrice()) {
            throw new UnsupportedOperationException("Cannot sell item " + item.getCode() + " below the reserve price");
        }
        return highestBid;
    }

    public List getItems() {
        return items;
    }

    public void addItems(List items) {
        this.items = items;
    }
}

Let’s test that an bid did not reach the reserve price for an item:

public class AuctionTest {

    private List buildItems() {
        List items = new ArrayList<>();
        items.add(new Item("code1", 100));
        items.add(new Item("code2", 1500));
        return items;
    }

    @Test
    public void testCannotSellItemAtAuction() {
        Auction auction = new Auction();
        auction.addItems(buildItems());
        Item goldWatch = auction.getItems().get(1);
        try {
            auction.sendBidForItem(goldWatch, 1000);
            auction.sendBidForItem(goldWatch, 800);
            assertEquals("There are 2 bids for the gold watch", auction.getAllBidsForItem(goldWatch).size(), 2);
            Bid winningBid = auction.sellItem(goldWatch);
        } catch (UnsupportedOperationException e) {
            assertEquals(e.getMessage(), "Cannot sell item " + goldWatch.getCode() + " below the reserve price");
        }
    }

Looks familiar? It will work without a problem. But we can do better.

@Test(expected = UnsupportedOperationException.class)
public void testCannotSellItemAtAuction2() {
    Auction auction = new Auction();
    auction.addItems(buildItems());
    Item goldWatch = auction.getItems().get(1);
    auction.sendBidForItem(goldWatch, 1000);
    auction.sendBidForItem(goldWatch, 800);
    assertEquals("There are 2 bids for the gold watch", auction.getAllBidsForItem(goldWatch).size(), 2);
    Bid winningBid = auction.sellItem(goldWatch);
}

Ok. No more try catch stuff. But we cannot check the exception message. Let’s give it another try.

@Test
public void testCannotSellItemAtAuction3() {
    Auction auction = new Auction();
    auction.addItems(buildItems());
    Item goldWatch = auction.getItems().get(1);
    auction.sendBidForItem(goldWatch, 1000);
    auction.sendBidForItem(goldWatch, 800);
    assertEquals("There are 2 bids for the gold watch", auction.getAllBidsForItem(goldWatch).size(), 2);
    expectedException.equals(UnsupportedOperationException.class);
    expectedException.expectMessage("Cannot sell item " + goldWatch.getCode() + " below the reserve price");
    Bid winningBid = auction.sellItem(goldWatch);
}

Better. The code is less verbose and more elegant. But what is expectedException? And the answer is: a Junit rule.

@Rule
public ExpectedException expectedException = ExpectedException.none();

What’s behind the scenes?

Junit creates a default statement which will be passed around and information will be added to it. The statement represents your test method in Junit environment. At this step @BeforeClass, @AfterClass and others will be determined. It will contain also the actual test methods. A new instance of the class with be created and the @Rule annotated methods and fields will be determined. On each rule found it will create a new instance of the rule statement and will call the evaluate method on it, which basically execute the tests. This is the place where your business code steps in.

Existing rules

  1. @TemporaryRuleFolder – allows creation and deletion of files and folders.

  2. @ExternalResource – setup an external resource (server, db connection)

  3. @ErrorCollector – allows to continue tests when something goes wrong

  4. @Verifier – can turn passing tests into failing tests

  5. @TestWatchman – a spy for test methods

  6. @TestName – can access test method name in tests

  7. @Timeout – applies same timeout for all test methods

  8. @ExpectedException – test exception types and messages

  9. @ClassRule – an extension for method rules that can be applied to classes

  10. @RuleChain – for ordering rules

More information and examples can be found on https://github.com/junit-team/junit4/wiki/Rules.

Custom rules

Cool. It’s pretty straightforward to create a custom rule. First step is to implement TestRule interface, which will need an implementation of apply() method. This will return a Statement. Your custom code must go into the statement’s evaluate() method. For the above example let’s create a simple rule that will provide us with Items.

public class ItemRule implements TestRule {

    private List items;

    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                //before
                buildItems();
                base.evaluate();
                //after
                System.out.println("Items created");
            }
        };
    }

    private List buildItems() {
        items = new ArrayList<>();
        items.add(new Item("code1", 100));
        items.add(new Item("code2", 1500));
        return items;
    }

    public List getItems() {
        return items;
    }
}

As you can see it’s pretty self explanatory what is going on here. You can do many things before and/or after the test is being run: mock objects, start connections, start a test in it’s own thread. Really it’s up to you. Think of it like an aspect(AOP). This is just a simple example to get you started.

@Rule
public ItemRule itemRule = new ItemRule();

private List buildItems() {
    return itemRule.getItems();
}

Now in Junit 5 it seems these rules were dropped, but let’s wait for a stable release to see exactly what is new and how can we migrate these rules according to the new API.

SOURCE CODE

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.