Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Complex Test Objects Made Easy in Java

DZone's Guide to

Complex Test Objects Made Easy in Java

Those complex test objects in Java don't have to be complicated. In fact, they can be made pretty easy, as they are in this neat tutorial.

· Java Zone
Free Resource

Managing a MongoDB deployment? Take a load off and live migrate to MongoDB Atlas, the official automated service, with little to no downtime.

One of the important goals of creating code using Test Driven Development (TDD) is to create a set of clear and readable test cases that can be used as the documentation of the system and the requirements that went into the original creation of the code. Whilst an admirable goal it can be quite difficult, particularly as soon as your objects become even slightly complex. A common code smell in tests comes from the creation of complex objects so that it is unclear which fields have caused the desired side effect. Let’s take this example test:

   @Test
    public void oldStyleTest() {
        Blotter blotter = new Blotter();

        blotter.add(
            new Trade(
                "buy",
                asList(
                    new Fill(100, 200, "timestamp")),
                "SYM",
                "VENUE",
                "Buy",
                100,
                1000,
                100,
                12345,
                "ACCOUNT",
                "Time",
                100,
                true)
        );

        blotter.add(new Trade(
                "buy",
                asList(
                    new Fill(100, 400, "timestamp")),
                "SYM",
                "VENUE",
                "Buy",
                100,
                1000,
                100,
                12345,
                "ACCOUNT",
                "Time",
                100,
                true)
        );

        Position position = blotter.positions().get(0);
        assertThat(position.averagePrice(), is(300));
    }

The majority of this test code is the creation of the complex Trade object! Whilst it could be extracted out into a field, it means the code becomes extremely messy if you have two or three of these objects, with different fields. The bigger question is, where has that 300 value come from? I have so many fields of data being setup I have no idea which data is relevant to the test. It’s not clear from the constructor and it makes the mean time to understanding extraneously long. We should be able to look at the test and understand quickly what it is doing.

There are certainly manual ways around this. Objects like this lend themselves well to the builder pattern. However, you still end up with the original creation of the object being exceptionally verbose, and it’s still necessary to create an original instance with all the fields set. Factories also have a lot of boilerplate code involved.


        TradeBuilder builder = new TradeBuilder().withDirection("buy").withSymbol("SYM").withVenue("VENUE”)….// rest of fields boilerplated

        tradeOne = builder.withQty("100").withPrice("200");
        tradeTwo = builder.withQty("100").withPrice("400");


        Position position = blotter.positionFor("SYM");
        assertThat(position.averagePrice(), is(300));

   public class TradeBuilder {
        private String direction;
        private String sym;

        public TradeBuilder withDirection(String buy) {
            this.direction = buy;
            return this;
        }

        public TradeBuilder withSymbol(String sym) {
            this.sym = sym;
            return this;
        }

        ...

        public TradeBuilder but(){
            return new TradeBuilder().withDirection(direction).withSymbol(sym).....
        }

        public Trade build(){
            return new Trade(orderType, symbol, venue, direction, originalQty, qty, price, id, account, ts, totalfilled, open)
        }
    }

Whilst we could provide default values in the builder this is a very bad idea if using the builder for production code. 


This is a problem I find I come across regularly in tests. Fortunately there is an easier way. Infamous Java developer Nat Pryce created “Make It Easy”, a library specifically for the goal of fixing this problem.

First, let’s look at what the final solution looks like.

 @Test
    public void makerTest() {
        Blotter blotter = new Blotter();
        Maker<Trade> baseTrade = 
            a(Trade, 
                with(symbol, "SYM"), 
                with(direction, "buy"));

        Trade tradeOne = make(baseTrade.but(
            with(qty, 100),
            with(price, 200)));

        Trade tradeTwo =  make(baseTrade.but(
            with(qty, 100),
            with(price, 400)));

        blotter.add(tradeOne);
        blotter.add(tradeTwo);

        Position position = blotter.positionFor("SYM");
        assertThat(position.averagePrice(), is(300));

    }

What I love about this syntax is that it’s clear what the test cares about; all the trades are for SYM symbol, and they’re all buy. The other fields can be anything, we don’t care; they’re not relevant. We know clearly how the two trades differ (in price) and how this is what we care about in this test.

The creation of the Maker class is simple as well.

   public static Property<Trade, String> orderType = new Property<>();
    public static Property<Trade, List<Fill>> fills = new Property<>();
    public static Property<Trade, String> symbol = new Property<>();
    public static Property<Trade, String> venue = new Property<>();
    public static Property<Trade, String> direction = new Property<>();
    public static Property<Trade, Integer> originalQty = new Property<>();
    public static Property<Trade, Integer> qty = new Property<>();
    public static Property<Trade, Integer> price = new Property<>();
    public static Property<Trade, Integer> id = new Property<>();
    public static Property<Trade, String> account = new Property<>();
    public static Property<Trade, String> ts = new Property<>();
    public static Property<Trade, Integer> totalFilled = new Property<>();
    public static Property<Trade, Boolean> open = new Property<>();

    public static final Instantiator<Trade> Trade = lookup -> {
        Trade trade = new Trade(
                lookup.valueOf(orderType, "fillorbust"),
                lookup.valueOf(fills, asList(new Fill(20, 200, ""))),
                lookup.valueOf(symbol, "GBR"),
                lookup.valueOf(venue, "ESSEX"),
                lookup.valueOf(direction, "buy"),
                lookup.valueOf(originalQty, 20),
                lookup.valueOf(qty, 20),
                lookup.valueOf(price, 200),
                lookup.valueOf(id, 1234),
                lookup.valueOf(account, "ABCXYZ"),
                lookup.valueOf(ts, "12d"),
                lookup.valueOf(totalFilled, 20),
                lookup.valueOf(open, false)
        );
        return trade;
    };

For each field of the Trade object we must create a static Property type, which maps the field name to the type. Whilst a fairly manual process, it’s surprisingly quick to do and immensely valuable if the Object is one that is like to be used in a lot of tests.

The static Instantiator then creates a new Trade Object; the values used will either be the default value provided here, or the value set using the maker. By putting sensible values here we can ensure the test focuses on the values that matter, and not on creating boilerplate values.

MakeItEasy is a wonderful, small and highly functional library for dramatically improving the readability of your test cases.

Use the comments section for any questions or suggestions, or follow me direct via @SambaHK on Twitter

MongoDB Atlas is the easiest way to run the fastest-growing database for modern applications — no installation, setup, or configuration required. Easily live migrate an existing workload or start with 512MB of storage for free.

Topics:
tdd ,java ,testing ,patterns ,clean code

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}