Question

I'm trying to simplify our unit tests with hand written DSL's. So far I have DSL's that walk developers through processing a service after setting up all preconditions and the construction of an monster object that has a huge constructor and setters that conflict. The monster is one of the many preconditions of processing the service and they constructed by hand over and over as developers write unit tests for new services in our system. So it would be worth putting considerable effort into making constructing them easier. I'm tried of watching people copy and paste the "life support system" from unit test to unit test.

I'm making a separate new classes, one who's job it is to build the monster class and one that walks them through the steps to process the service. These will be used by many developers. This means once it's used widely changes will be difficult. That's why I'm worried about extendability.

Rather than a simple fluent interface builder (where every method returns a this and is always available) I've decided to use an internal DSL that returns a different nested class. This is a very powerful choice. It lets me limit the methods available at any one step to only the valid ones. It also lets me change what the return type of a method is based on the "state" previous choices put the DSL into.

Briefly, the way it works is the first method returns the first nested class that has the next valid method that could not have been called before. This limits the choices intelisense will offer making writing the DSL's calling code easier.

//Final build() method doesn't exist on PersonBuilder.  Its on a nested class.
public class PersonBuilder {
    public GotRequired doRequired() {
        return new GotRequired();
    }

    //One of many nested classes 
    public class GotRequired {
        public GotFirstName addFirstName(String firstName) {
            mFirstName = firstName;
            return new GotFirstName();
        }
    }

    public class GotFirstName {
        public GotMiddleName addMiddleName(String middleName) {
            mMiddleName = middleName;
            return new GotMiddleName();
        }
    }

    ...

}

This can be used to ensure methods are called in some order and ensure that preconditions have been met. The inner classes can be thought of as states the DSL is in. More than one state change path can be supported. Simply being in a state can tell you what states came before so long as no state merges two state paths into one. This all just means if you're careful you can know every needed method was called before building or returning a result.

Fair warning, what follows is java verbose boiler plate at it's finest. The full listing is here: Monster builder code.

The monsters DSL code looks something like this:

public class PersonBuilder {

// -- Required -- //

private String   mFirstName;
private String   mMiddleName;
...

// -- Required Alternatives -- //  

//Not both; not neither
private int      mBeersToday;     //One or
private String   mHowDrunk;       //the other


// -- What all the fuss is about -- //

private Person   mPerson;

/** Call each required method in order offered.*/
public GotRequired doRequired() {
    return new GotRequired();
}

public class GotRequired {
    public GotFirstName addFirstName(String firstName) {
        mFirstName = firstName;
        return new GotFirstName();
    }
}

public class GotFirstName {
    public GotMiddleName addMiddleName(String middleName) {
        mMiddleName = middleName;
        return new GotMiddleName();
    }
}

...

/*
   Client code example


    .doRequiredAlternatives()   //Either x or y. a, b, or c. etc.
        .addBeersToday(3)       //Now can't call addHowDrunk("Hammered");
        .addFavoriteBeer("Duff")//Now can’t call addJobTitle("Safety Inspector");  
   .addBuild();                 //Calls different constructors based on alternatives

*/
    /** Now the interesting forking bit */
    public class GotRequiredAlternatives {
        public GotPerson addBeersToday(int beersToday){
            mBeersToday = beersToday;

            //Got enough for constructor
            mPerson = new Person(
                    mFirstName,
                    mMiddleName, 
                    mLastName,

                    mNickName,                        
                    mMaidenName, 
                    mEyeColor,   
                    mHairColor,  
                    mDateOfBirth,
                    mBeersToday, //GotBeersToday
                    mAliases);

            return new GotPerson();
        }        

        public GotPerson addHowDrunk(String howDrunk){
            mHowDrunk = howDrunk;

            //Got enough for constructor
            mPerson = new Person(
                    mFirstName,
                    mMiddleName, 
                    mLastName,

                    mNickName,                        
                    mMaidenName, 
                    mEyeColor,   
                    mHairColor,  
                    mDateOfBirth,
                    mHowDrunk, //GotHowDrunk
                    mAliases);

            return new GotPerson();
        }        
    }

    //Could have created GotHowDrunk and GotBeersToday 
    //but we're past the constructor choice so we can 
    //forget the path that brought us here.

    public class GotPerson {
        /** Build Person object leaving optional fields set to default values */
        public Person doBuild() {
            return mPerson;
        }
        /** Call any or none of these optional fields */
        public GotOptional doOptional() {
            return new GotOptional();        
        }
    }

    //Since person is not immutable the only thing gained here is confidence that these were set according to persons whacky rules
    class GotOptional {

        /** Build Person object */
        public Person doBuild() {
            return mPerson;
        }

        public GotOptionalAlternatives doOptionalAlternatives() {
            return new GotOptionalAlternatives();
        }

        public GotOptional addClothing(String clothing) {
            mPerson.setClothing(clothing);
            return this; 
        }

        public GotOptional addTatoo(String tattoo) {
            mPerson.setTattoo(tattoo);
            return this; 
        }

        //Add any number of setters that have good default values and do not conflict with each other

    }

    //Ideally person wouldn't allow this anyway but person is set in stone so at least this provides a safer interface
    public class GotOptionalAlternatives {

        /** Build Person object */
        public Person doBuild() {            
            return mPerson;
        }

        //Optional but conflicting setters. Might never be called.  Must never be called together.


        public GotFavoriteBeer addFavoriteBeer(String favoriteBeer){
            //mFavoriteBeer = favoriteBeer;  //TODO remove
            mPerson.setFavoriteBeer(favoriteBeer); //GotFavoriteBeer

            return new GotFavoriteBeer();
        }

        public GotJobTitle addJobTitle(String jobTitle){
            //mJobTitle = jobTitle;  //TODO remove
            mPerson.setJobTitle(jobTitle); //GotJobTitle

            return new GotJobTitle();
        }
    }

    //These are not strictly needed.  They are like one-statement {}'s after an 'if'.  Simply there if more gets added.

    public class GotFavoriteBeer {
        /** Build Person object */
        public Person doBuild() {            
            return mPerson;
        }
    }

    public class GotJobTitle {
        /** Build Person object */
        public Person doBuild() {            
            return mPerson;
        }
    }
}

I'll spare you the service code that uses the same nested class technique to mix optional methods and required methods. It is also able to return different result types based on previous alternate method choices since even though the result() method has the same name and params it's coming off a different inner class.

Despite the unreal amount of typing required to create these DSLs I've found the results to be very powerful. However, I expect people will want new features added, new preconditions for processing the service. If I use this style is there any hope of reuse or would it be better to just write a whole new DSL each time something needs to change?

Cause once it's working and being used I do NOT want to dive back into the existing DSL classes and mess with them.

If you can see other issues please let me know. I'm hoping for a peer review but I appreciate any feedback.

Was it helpful?

Solution

Here's the thing with unit tests. You want them to be really easy to write. Anytime writing a test is made more difficult, it will result in tests not being written, and bugs will be allowed to live. So let's imagine I'm writing a test:

void testCanDrive() {
    Person person = new PersonBuilder()
        .doRequired()
        .addFirstName("John")
        .addMiddleName("Q")
        .addLastName("Smith")
        .addNickName("Johnny")
        .addMaidenName("Public")
        .addEyeColor("Blue")
        .addHairColor("Blond")
        .addDateOfBirth(new Date(1990, 1, 14))
        .addAliases()
        .doRequiredAlternatives()
        .addBeersToday(1)
        .doBuild();
    assertFalse(person.canDrive());
}

Here's the thing, I had to write a crazy amount of code to create the person object, and I didn't really care about most of those details. The only factors that would seem relevant to whether you can drive would be your date of birth and the beers you've had. Forcing the test writer to specify all of that other details makes it harder to follow the test and discourages me from writing further tests.

Now your solution is probably way better than the status quo, but it still sucks. I would suggest that you really want to use a typical builder interface that set default values for everything. Each test will only set those details pertinent to the test itself, for everything else the defaults should be fine.

void testCanDrive() {
    Person person = new PersonBuilder()
        .setDateOfBirth(new Date(1990, 1, 14))
        .setBeersToday(1)
        .doBuild();
    assertFalse(person.canDrive());
}
Licensed under: CC-BY-SA with attribution
scroll top