It’s been a while since I wrote about Cucumber, but hopefully I’m not too rusty. As I mentioned in my last post, I’ve recently gotten back into Cucumber. For my current client, I am developing a framework which allows testing the same behavior on multiple applications. To me, this is reminiscent of my first exposure to Cucumber, using it to run through similar behaviors on the web, services, and a database. I previously espoused on multiple best practices of using Cucumber, including tagging, background steps and hooks, and general best practices. In this post, I plan to expand on some of the best practices of glue code.

Organize Your Test Steps

Keep it DRY, “Don’t Repeat Yourself.” You’ll hear this a lot when writing code, and guess what, your glue code is code! So, what does this mean for our Cucumber tests? Ensuring you have one test step for each action that you are trying to accomplish, and that the information contained within, has the appropriate flexibility.

One personal pet peeve of mine is code that is poorly organized. While I’m just as guilty of this as any other developer, knowing where to find a certain statement logically in a growing codebase is important. Following this, I like to organize my glue code based on functionality, with steps in the appropriate, separate classes. If I’m writing WebDriver tests, I like my glue code to follow the same breakup/structure as my POM (Page Object Model for those uninitiated).

Well, one drawback to Cucumber is when you break up your test steps, you often need a way to pass variables and data to these separated classes. Cucumber Step classes don’t have, nor support a traditional constructor, allowing the passing in of variables. It’s important to ensure the same objects are being used across multiple Step classes, after all, for a WebDriver test, we don’t want separate Step classes running on separate browsers. This might even be some user you are acting on or some page data you are collecting to be later verified or re-used. In order to split these steps out, but still retain access to the data, this leaves you with two options typically: extending a base method, or using Dependency Injectors.

I’ve run into more than a few occasions when I’ve wanted to create some test steps as interfaces (setup different browsers, set up different workflows, etc), and as hopefully you know, interfaces can’t extend classes. So this leaves us with the need to use Dependency Injectors. Personally, I like using PicoContainer. It’s simple, no annotations are needed, and it gets the job done.

What is a Dependency Injector

At their core, Dependency Injectors (DIs) are about being able to extend an object’s behavior at runtime by injecting business logic. While this allows you to do a lot of awesome things, for this post, we’re going to discuss one particular capability that DIs allow: passing objects from one class to another, instead of hardcoding values or steps. There are a lot of different DI tools out there; this post will focus on PicoContainer, mainly due to its simplicity, and purposeful design for Cucumber. For more on Dependency Injectors, I’d suggest reading through some of Martin Fowler’s posts. PicoContainer’s most important feature is its ability to instantiate arbitrary objects. This is done through its API, which is similar to a hash table. You can put java.lang.Class objects in and get object instances back.

Most of all that is needed to know to use DIs effectively in Cucumber is to set up classes which need objects passed in with these objects declared in their constructor.

    public LoginSteps(Controller controller, User user) {
        this.controller = controller;
        this.user = user;
    }

This code, at its heart, takes two objects, and stores them, for later usage in test steps. In this example, our controller contains our WebDriver object, which has previously been instantiated in a @Before step, also with the same DI setup. This means all our Step classes have access to the same WebDriver object, and can all perform the same actions on our browser. One major thing to note is that both Controller and User don’t have constructors. This is important, as otherwise, PicoContainer would not be able to create an instance of these objects and provide the same instance to each class looking for it.

DI is wonderful for simplifying code and ensures that values don’t need to be hard-coded or accessed in any kludgey fashion. Your code will look cleaner, you’ll find less duplication, and ultimately it should be easier to maintain.

Some Sample Code

Below are some snippets from a potential Maven Java project.

Setting up your pom

Just include Picocontainer

    …
    <dependencies>
        …
        <!-- https://mvnrepository.com/artifact/info.cukes/cucumber-picocontainer -->
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-picocontainer</artifactId>
            <version>1.2.5</version>
        </dependency>
        …

Setting up a shared WebDriver Instance

Create your WebDriver class

public class Controller {
    private WebDriver driver;

    public WebDriver getDriver() {
        return driver;
    }

    public void setDriver(WebDriver driver) {
        this.driver = driver;
    }

    public void setupController() {
        this.driver = new ChromeDriver();
    }

    public void teardownController() {
        if (driver != null) {
            driver.quit();
        }
    }
}

Instantiate the driver in one Steps class

public class SetupSteps {
    Controller controller;
    User user;

    public Setup(Controller controller, User user) {
        this.controller = controller;
        this.user = user;
    }

    @Before
    public void setup() {
        controller.setupController();
    }

    @After
    public void teardown() {
        controller.teardownController();
    }
}

Finally, let’s reuse the controller in another class

public class LoginSteps {

    Controller controller;
    User user;
    LoginWorkflow loginPage;

    public LoginSteps(Controller controller, User user) {
        this.controller = controller;
        this.user = user;
        loginPage = new LoginPage(this.controller);
    }

    @When("^I login$")
    public void login() {
        this.loginPage.loadEnvironment();
        this.loginPage.login(this.user);
    }
}

No mess. No extends. Just very clean, simple code. Note, nothing needed to be called to get instantiated, between Cucumber and PicoContainer, this is all handled as part of the framework, with no additional code needed.

Using PicoContainer for Interfaces

One of the main points I made above, is that having your test step classes extend a base class doesn’t work if you test step classes are interfaces. So, how would one do that?
First, we need to tell PicoContainer to use a custom factory. To do that, in your src/test/resources directory, create a file called cucumber.properties. The contents of that file should just point to your custom factory, that defines your interface implementation:

cucumber.api.java.ObjectFactory=com.sample.controllers.CustomPicoFactory

Then, define your interface, and some implementations

public interface Controller {
    public Device getDevice();
    public WebDriver getDriver();
    public void setDriver(WebDriver driver);

    public void setupController();
    public default void teardownController() {
        if (getDriver() != null) {
            getDriver().quit();
        }
    }
}

public class ChromeController implements Controller {
    private Device device = Device.CHROME;
    private WebDriver driver;
    @override
    public Device getDevice() {
        return device;
    }
    @override
    public WebDriver getDriver() {
        return driver;
    }
    @override
    public void setDriver(WebDriver driver) {
        this.driver = driver;
    }
    @override
    public void setupController() {
        this.driver = new ChromeDriver();
        setupLogging();
    }
}

public class FirefoxController implements Controller {
    private Device device = Device.FIREFOX;
    private WebDriver driver;
    @override
    public Device getDevice() {
        return device;
    }
    @override
    public WebDriver getDriver() {
        return driver;
    }
    @override
    public void setDriver(WebDriver driver) {
        this.driver = driver;
    }
    @override
    public void setupController() {
        this.driver = new FirefoxDriver();
        setupLogging();
    }
}

The above code, is setting up the a controller with a WebDriver object, based on the device provided. In this case, it’s being provided as a system property – passed into Maven as -Ddevice=chrome. All of this is handled via a custom Device class.

public enum Device {
    FIREFOX, CHROME;

    public static final Logger log = Logger.getLogger(Device.class);

    /**
     * allows the browser selected to be passed in with a case insensitive name
     *
     * @param b - the string name of the browser
     * @return Browser: the enum version of the browser
     * @throws InvalidDeviceException If a browser that is not one specified in the
     *                                Selenium.Browser class is used, this exception will be thrown
     */
    public static Device lookup(String b) throws InvalidDeviceException {
        for (Device browser : Device.values()) {
            if (browser.name().equalsIgnoreCase(b)) {
                return browser;
            }
        }
        throw new InvalidDeviceException("The selected device " + b + " is not an applicable choice");
    }

    public static Device getDevice() {
        Device device = Device.FIREFOX;
        if (System.getProperty("device") != null) {
            try {
                device = Device.lookup(System.getProperty("device"));
            } catch (Exception e) {
                log.warn("Provided device does not match options. Using Firefox instead. " + e);
            }
        }
        return device;
    }
}

Finally, the last step is to set up our factory defined in properties file above. For our example, it would look something like this:

public class CustomPicoFactory extends PicoFactory {
    public CustomPicoFactory() {
        switch (Device.getDevice()) {
            case FIREFOX:
                addClass(FirefoxController.class);
                break;
            case CHROME:
                addClass(ChromeController.class);
                break;
            default: // if no device is specified, use firefox
                addClass(FirefoxController.class);
        }
    }
}

And of course, we might want to add more implementations of our Controller class for a more complex factory above.

Hopefully, this will get you started down the right track, writing good, clean code. Leave comments below, and of course happy testing!

Leave a comment

Your email address will not be published. Required fields are marked *

X