Blog-Archiv

Donnerstag, 17. September 2015

Visitor Pattern for Test Data in Java

A visitor is one that comes in and is not interested to know how the exhibition is organized. He wants to see the pictures. So the gallery guide leads him through all floors and rooms.

  • Finally the visitor
    • did not miss any picture because of the guidance, and
    • did not have to care about finding the next picture, room or floor of the gallery,
  • and the gallery owner
    • can always put the pictures into dry and cool rooms because visitors do not expect them to be at a certain place.

The Visitor Design Pattern is made for iterating through structures like lists, trees or graphs, without having to know how to get to the next item, thus providing great re-organization freedom for the data structure. Moreover it is a technique that allows type-safe iteration through "heterogenous" structures, that means the items can be of different type: paintings, photos, sculptures. At any item, the visitor would be called with its visit(item) method that declares the type of the currently visited item: visit(painting), visit(photo), visit(sculpture). Thus the visitor pattern is great for avoiding the instanceof operator.

The pattern defines two participants:

  1. the visitor and
  2. the data structure to be visited.

The visitor has to implement an interface, while the visit-able structure must provide an accept(visitor) method by which the visitor can enter the gallery.

The following article rolls out an example Java application that shows how test data can be organized and visited for different purposes in a unit test.

Test Data Set

The concept is to have

  • the id of an UI-field and
  • an intended value for that field

stored together in some visit-able data structure. This structure could be first

(1) visited by a visitor that puts the values into the according UI-fields (e.g. using Selenium).

Then the test would press the "Save" button and reload the UI. Without anticipating any persistence medium like database or filesystem, it then could secondly

(2) visit the structure by a visitor which asserts that the values match those in the according UI-fields.

This would be a meaningful unit test. The two visitors would have different tasks to do, but use the same iteration mechanism to perform their task. The advantage is that you don't need to duplicate the test data for (1) insertion and (2) assertion.

Here is the first outline of the data structure. It wraps a simple array of test data tuples (without duplicates, thus it is called "Set"). The term entity is used as synonym for what we know as "database record".

/**
 * Represents a set of test-data tuples.
 */
public class TestDataSet
{
    /**
     * Abstraction of a test-data tuple.
     */
    public abstract static class FieldAndValue
    {
        public final String fieldId;
        
        protected FieldAndValue(String fieldId)    {
            this.fieldId = fieldId;
        }
    }

    public final String uiEntityIdentifier;
    private FieldAndValue [] fieldsAndValues;

    /**
     * @param uiEntityIdentifier
     *         the "UI natural key" for the entity the test-data should go into,
     *         we want be able to re-load the entity by that key for assertion.
     *         For creating and re-loading the person "John Doe" it could be f.i. "Doe".
     * @param fieldsAndValues the test data tuples.
     */
    public TestDataSet(String uiEntityIdentifier, FieldAndValue [] fieldsAndValues)    {
        this.uiEntityIdentifier = uiEntityIdentifier;
        this.fieldsAndValues = fieldsAndValues;
    }
}

As you might have noticed, the FieldAndValue class does not contain a value, not even as abstraction, just the fieldId.

Because the tuples will have values of different data-types, I can not provide the value in the abstraction (except maybe as Object, but I want to avoid cast operations as much as possible). Here are my tuple-classes for three types of input fields:

  • textfield,
  • checkbox,
  • chooser (combobox, selectionlist, however you want to call the controls with predefined values)
public class TestDataSet
{
    .....

    /** Text field and value. */
    public static class StringFieldAndValue extends FieldAndValue
    {
        public final String value;
        
        public StringFieldAndValue(String fieldId, String value) {
            super(fieldId);
            this.value = value;
        }
    }
    
    /** Checkbox field and value. */
    public static class BooleanFieldAndValue extends FieldAndValue
    {
        public final boolean value;
        
        public BooleanFieldAndValue(String fieldId, boolean value) {
            super(fieldId);
            this.value = value;
        }
    }
    
    /** Combobox field and value. */
    public static class ChooserFieldAndValue extends StringFieldAndValue
    {
        public ChooserFieldAndValue(String fieldId, String value) {
            super(fieldId, value);
        }
    }

    .....
}

The public fields of this structure are immutable due to their final modifier. This is important to prevent the case that the insertion uses other data than the assertion, these two should be bound tightly together.
(Hint: never make public fields without the final modifier!)

Any constructor of TestDataSet will use instances of these inner classes to build the contents of the data structure.

Data Accept Visitors

The data structure is ready for use now, but it can not be visited. Moreover an interface is needed that defines the types a visitor can visit.

public class TestDataSet
{
    /**
     * Implementers visit test data without having to
     * cast to StringFieldAndValue or BooleanFieldAndValue.
     */
    public static abstract class Visitor
    {
        /**
         * Called before visitation starts. Does nothing. To be overridden for preparing the UI.
         * @param uiEntityIdentifier the uiEntityIdentifier of the TestDataSet to be executed.
         */
        public void init(String uiEntityIdentifier)    {
        }
        
        /** Visits any textfield-bound tuple in the set. */
        public abstract void visit(StringFieldAndValue stringFieldAndValue);
        
        /** Visits any checkbox-bound tuple in the set. */
        public abstract void visit(BooleanFieldAndValue booleanFieldAndValue);
        
        /** Visits any chooser-bound tuple in the set. */
        public abstract void visit(ChooserFieldAndValue chooserFieldAndValue);
    }
    
    .....
    .....

    public void accept(Visitor visitor)    {
        visitor.init(uiEntityIdentifier);
        
        for (FieldAndValue fieldsAndValue : fieldsAndValues)
            fieldsAndValue.accept(visitor);
    }

}

Here we have a type-safe access to the different tuple classes. Every visitor will be able to use the boolean field-value as well as the String field-value without any type-cast or instanceof operator.

The init() method is for performing optional preparations on the UI, e.g. pressing a "New" button, or loading a certain URL. It is not part of the original Visitor Design Pattern.

The accept() implementation is just a simple loop over the items that were passed to the constructor. We could extend it to a tree traversal at any time later.

One thing is missing: also the tuple classes must accept visitors. The visit() implementation always must be in the type that is to be visited. These are the ones declared in the Visitor interface.

public class TestDataSet
{
    ....
    
    /** Base class, holding just the field-id. */
    public abstract static class FieldAndValue
    {
        public final String fieldId;
        
        protected FieldAndValue(String fieldId)    {
            assert StringUtils.isNotEmpty(fieldId);
            this.fieldId = fieldId;
        }
        
        abstract void accept(Visitor visitor);
    }
    
    /** Text field and value. */
    public static class StringFieldAndValue extends FieldAndValue
    {
        public final String value;
        
        public StringFieldAndValue(String fieldId, String value) {
            super(fieldId);
            this.value = value;
        }
        
        @Override
        void accept(Visitor visitor)    {
            visitor.visit(this);
        }
    }
    
    /** Checkbox field and value. */
    public static class BooleanFieldAndValue extends FieldAndValue
    {
        public final boolean value;
        
        public BooleanFieldAndValue(String fieldId, boolean value) {
            super(fieldId);
            this.value = value;
        }
        
        @Override
        void accept(Visitor visitor)    {
            visitor.visit(this);
        }
    }
    
    /** Combo-box and reference-chooser field and value. */
    public static class ChooserFieldAndValue extends StringFieldAndValue
    {
        public ChooserFieldAndValue(String fieldId, String value) {
            super(fieldId, value);
        }
        
        @Override
        void accept(Visitor visitor)    {
            visitor.visit(this);
        }
    }

    ....
    
}

Looks a little repetitive, but is not possible otherwise. The abstract FieldAndValue super-class defines the abstraction of the accept() method, and all sub-classes must implement it and pass themselves to the visitor.

Now it is complete, ready to be used. If you put together all parts of the TestDataSet class now, you will be able to implement test cases. But first we need the visitor that does the insertion, and the visitor that does the subsequent assertion.

Visitors for Insertion and Assertion

Let's assume that we have a class SeleniumTest that provides following basic methods:

  • loadPage(String url) - load given application
  • clickRow(String itemText) - select an entity offered in a chooser-UI

and following field-related write-methods:

  • type(String stringFieldId, String value)
  • click(String checkboxFieldId, boolean value)
  • choose(String chooserFieldId, String itemText)

and following field-related read-methods:

  • String getTextValue(String stringFieldId)
  • boolean getBooleanValue(String checkboxFieldId)
  • String getChooserValue(String chooserFieldId)

Then we could implement the insertion-and assertion-visitors:

public class SeleniumTest extends TestCase
{
    protected void loadPage(String url) {
        .....
    }
    protected void clickRow(String itemText) {
        .....
    }
    
    protected void type(String textFieldId, String value) {
        .....
    }
    protected void click(String checkboxId, boolean value) {
        .....
    }
    protected void choose(String chooserId, String itemText) {
        .....
    }
    
    protected String getTextValue(String textFieldId) {
        .....
    }
    protected boolean getBooleanValue(String checkboxId) {
        .....
    }
    protected String getChooserValue(String chooserId) {
        .....
    }
    
    protected class TestDataInsertVisitor extends TestDataSet.Visitor
    {
        @Override
        public void visit(TestDataSet.StringFieldAndValue f) {
            type(f.fieldId, f.value);
        }
        @Override
        public void visit(TestDataSet.BooleanFieldAndValue f) {
            click(f.fieldId, f.value);
        }
        @Override
        public void visit(TestDataSet.ChooserFieldAndValue f) {
            choose(f.fieldId, f.value);
        }
    }

    public class TestDataAssertVisitor extends TestDataSet.Visitor
    {
        @Override
        public void visit(TestDataSet.StringFieldAndValue f) {
            assertEquals(f.value, getTextValue(f.fieldId));
        }
        @Override
        public void visit(TestDataSet.BooleanFieldAndValue f) {
            assertEquals(f.value, getBooleanValue(f.fieldId));
        }
        @Override
        public void visit(TestDataSet.ChooserFieldAndValue f) {
            assertEquals(f.value, getChooserValue(f.fieldId));
        }
    }

}

You will need to meditate about Selenium and the structure of your UI to implement the ..... methods. But when you're over that, we finally could write a short and elegant unit test, defining your test data just once-and-only-once:

public class PersonUiTest extends SeleniumTest
{
    public void testPersonUi() {
        // construction of test data
        final String uiEntityIdentifier = "Doe";
        final TestDataSet testData = new TestDataSet(
            uiEntityIdentifier,
            new TestDataSet.FieldAndValue [] {
                new TestDataSet.StringFieldAndValue("lastName", uiEntityIdentifier),
                new TestDataSet.StringFieldAndValue("firstName", "John"),
                new TestDataSet.ChooserFieldAndValue("country", "Antarctis"),
                new TestDataSet.BooleanFieldAndValue("internet", true),
                .....
                .....
            }
        );
        
        // insertion of test data
        testData.accept(new TestDataInsertVisitor()    {
            @Override
            public void init(String uiEntityIdentifier) {
                loadPage("createPerson");
            }
        });
        
        click("Save"); // data should be reloaded from persistence layer into UI now
        
        // assertion of test data
        testData.accept(new TestDataAssertVisitor()    {
            @Override
            public void init(String uiEntityIdentifier) {
                loadPage("choosePerson");
                clickRow(uiEntityIdentifier);
            }
        });
    }

}

Here we load a page where we can create a new person. With our TestDataSet instance that defines the person's data we perform the insertion by simply using a predefined insertion-visitor that knows how to bring the data into the UI. The init() override loads the page where we can do this.

Then we click onto the "Save" button and assume that the data get reloaded into the UI. For the assertion we simply call another visitation, using a predefined assertion-visitor that knows how to read data from fields and compare them to the according data values. The init() override loads a chooser page where the created person is selected for editing.

Thoughts about the Visitor Pattern

You might say that this pattern hardcodes and duplicates the data types that are contained in the visited structure. Any time you add another type (FieldAndValue sub-class) to that structure, you also have to declare a new Visitor interface method, and adapt all implementations to it.

You might also say that, although this pattern avoids the usage of the instanceof operator, the resulting code is less understandable than a simple loop that uses instanceof.

You may even say that design-patterns make an application unnecessarily complicated.
Nevertheless applications of this very thin pattern are elegant and type-safe. It is one of the patterns I use really frequently, because it separates the visited structure from the visitor in a way that gives that structure any freedom in maintenance. The visitor implementations will not need to be changed when the test data structure grows from a simple list into a tree or a graph. And that is really valuable.

Only forgetting to adapt the Visitor interface when a new data type is in the structure may create problems. Amazingly the instanceof solution shines in this case, because you could throw an exception when the type is unknown to the iterator:

if (f instanceof StringFieldAndValue)
    ....
else if (f instanceof BooleanFieldAndValue)
    ....
else if (f instanceof ChooserFieldAndValue)
    ....
else
    throw new IllegalArgumentException("Unknown type: "+f);





Keine Kommentare: