Thursday, May 25, 2017

Excelling at CucumberJVM GLOBAL Step Definitions

Cucumber is going global baby!  They've a vision that any and all definitions are available at the beck and call of any feature file.  Other BDD implementations allow static linking of feature file to a specific set of code of definitions.  Let's give the idea of global definitions a try with CucumberJVM (Java) and see what we can learn, cover tools and tricks to work with them, and look at how to design the definition code to be maintainable.
(XXX Link to example code on GitHub goes here.)

Feature files, Steps, Definitions, and Test Runners, oh my!

Feature files are text files containing the BDD (well really Gherkin) Steps such as Give, When, Then.  Definitions are built in programming languages and define what those steps mean.  Test Runners (such as JUnit, or cucumber command line) launch a program that looks for feature files, parses the feature file, and execute each Step by executing a Definition that matches the Step.  BDD Test frameworks usually allow some configuration of how to match a Step with a Definition.  Cucumber's vision is that all Definitions should be global and that the feature file should contain enough context to do this correctly.

Feature file for planning

This is the feature file used in planning.  It reads pretty well and gives a team a starting point for conversations on a point of sales feature for a pet store.
Buying a dog at a pet store such as Petco give you deals such as this.
(click pic to enlarge)
After planning with a number of such documented features, Once the Sprint started, during development of test automation, I realized I needed more context.  If the feature file alone was going to "drive" definition discovery, having descriptive columns wasn't going to give me enough differentiation across all steps in a global context.  example:
When purchasing a "selected accessory"
would generate a match for all other definitions with the words "When purchasing a," totally missing the important piece "selected accessory."
The Natural editor reporting multiple Definitions matching this Step.

Adjusting the feature file by pulling the descriptive columns "out" will allow us to work with global definitions.

How to know a Step has enough "closure?"

First off, we'll never be perfect as reality always brings new adventure. But you'll get closer faster by: reading each step alone, ignoring the context of the scenario title, feature file name, and feature file location.  This is how I realized that "When purchasing a" had too little context as the nice context of the column name wasn't going to help me.

So global definitions will make your feature files a little more wordy.  And you will be forced to make decisions in the future when you discover collisions.  Feature file editors like Natural will complain to you when you add one that has ambiguous definitions.

Test Automation Design

First off, let's get a feedback loop working.  (My code is on GitHub at: XXX)  Put the feature file into source control, add a test runner (or if you got the cucumber plugin working in eclipse, that will work too), and execute your test.  Observe that the feature file is executed but the scenarios are skipped as there are no definitions.  Also the console will give you stub code for the methods.

Organize feature files in a sensible hierarchy

JUnit test runner

JUnit test case which hands off to Cucumber
Natural gives feedback that definitions are missing
(If you add definitions, sometimes the feature file needs to
be reopened to force Natural to repairs and check.)
Global definitions change how automation is designed and built.  When using BDD tools that allow the developer to control linking Steps to Definitions, you'd typically see a one to one mapping of feature file to the java file containing the class of definitions:
With Cucumber, you're encouraged to build classes in this manner:
Although this explosion of smaller objects isn't necessary a bad thing, it leaves us with a problem:  "When" at line 8 needs to communicate with the "Then" at line 9, so these smaller objects need a way to communicate with each other.  A Singleton pattern could do this but puts more burden on the programmer, as now lifecycle management needs to be done to maintain isolation between tests (so that running one scenario doesn't cause a side affect with another scenario due to mismanaged state in a Singleton).  A better alternative is to work with Cucumber's lifecycle for doing this via dependency injection.

Working inside a World

The World lifecycle pattern is simple: the state to be shared between Definitions is stored in the World, and the world is created at the start of executing a scenario and then destroyed upon completion of the scenario.  Upon execution of a new scenario, a new World is created again, and so on.  Although the World pattern is heavily emphasized in Cucumber.JS, it's not so explicit for CucumberJVM.  Cucumber manages the World lifecycle automatically if you use Dependency Injection.  PicoContainer (built by the authors of Cucumber) is a simple and lite weight framework that gets the job done via constructor injection.

Here's how

Add picocontainer to your build dependencies (Because I found hard to work with, I used to search for the latest versions of "cucumber-picocontainer" and "picocontainer."):
Add two jars to activate World lifecycle and Dependency Injection
Take a look at your three definitions and create a new class for passing information.
Three Step Definitions
Since in this case it's about a shopping cart of items, lets go with that.
For now put the object in the same package as its steps in the top level package for definitions.  Later we'll reorganize but for now keep writing code because it will be easier to re-organize after more of the design has emerged.  Since PicoContainer uses constructor injection, add Constructors for the data injection.
Constructor Injection
(Click pic for larger resolution)
This is all the "structure" code needed for PicoContainer and Cucumber.  When Cucumber executes a feature file with these steps, and it matches to these definitions, it will use PicoContainer to find and inject the dependencies when it constructs these classes, and these dependencies will be inserted in the World during Scenario execution.

Here are the Given, When, Then definitions using the dependency:

 Execute the test runner and you'll see this is enough for the first scenario outline.

To illustrate the World is being destroyed, a short experiment such as injecting a counter to count how many times the Given is called will make this clear.

Counter is always one because it's replaced each time the Scenario is run
(Click to see full size pic.)
Although Counter is always incremented in the definition for Given, and checked in the definition for the Then, it is always set to 1 because each row of a scenario outline gets it's own World.

Ramifications of Global Definitions on Design

A good design does at least these two things well (in this order) that allow a program to respond to change:
1) communicates intent in an understandable way,  and
2) is maintainable.
Another 50 pages could be written about other important characteristics--the book Clean Code is a good reference--but let's keep it to the point: we don't program in binary because it's difficult to understand intent and if we wrote in binary anyhow, eventually you'll be hating life when you have to respond to new requirements.

Global definitions mean our feature files could have a relationship with any definition (Java class with a @Given, @When, @Then.  So organize the features files in a way that makes the feature files an index into your product's features.  Feature files will be the index into your definitions (Java code) as well.  To organize the java code so it communicates intent and is maintainable, use the principle of "keeping things that work together next to each other."  Said another way, keep definitions grouped with the things your injecting into them.  This is a big departure from BDD frameworks that don't do global definitions, where usually the feature files and definitions are grouped together in "src/java/com/feature/purchase/buydog."  With global definitions, doing so would actually misinform.  With global definitions, it'd be better to drop everything in one namespace.  But lets try something better than that.

For example, organize feature files thusly (there may not be an advantage to having feature files children of src/java directory, but I did this out of habit):

For Java code, I looked at each set of definitions and their collaborators and tried to group them in a sensible way:
Then later, when I added automation for the few selected accessories a few are not

I had to decide if I wanted to collaborate between the When and Then with the shopping cart, I needed to drop them in the same "shopping" namespace as the previous scenario.  The fact that I'm using the same Given definition, then that reinforces that decision.  This all makes sense since they are all about the same thing.  But get used to the idea that just because steps are in the same feature file, their definitions could be anywhere.

Times goes on keep the stair rails polished

Since feature files are a reflection of a product's features, BDD test automation needs to respond to three kinds of changes:
  • new behaviors/features, 
  • adjusting existing behaviors/features, and 
  • adjust how existing behavior/feature operates.

New behaviors versus adjusting existing behaviors

Organize feature files in a sensible hierarchy with good feature file names so it's easily browsable and searchable in order to answer the question, "is this new idea the PO has a new behavior or a change in an existing behavior?"
(It'll be hard to organize features without knowing the business you're building behaviors for.  Go find someone to help/interview about that as this knowledge typically isn't in the IT part of the organization.)

The business want's to collect customer contact info so they can send them offers via physical mail or email.  To do that, they make this offer to the customer at time of checkout by offer VIP cards that give an additional 5% discount on purchases to collect your contact info and send you more offers. 
If it's known that there will be ten more VIP card behaviors, better to make a directory just for different VIP features.  But if all we know at the time is that there is just this one feature, then we can just add the behavior into an existing feature file as shown (we can always move the feature files around later).
Adding another scenario outline (highlighted) to BuyDog
The global definitions related to these steps need to be updated to pass information about the VIP card status (the definitions for the Given and Then).  Because the definitions are global, finding the impacted definitions should be driven from the feature file.  If you've a good feature file editor like Natural, you can open those steps so you can implement a good way to inject (via PicoContainer) an object to pass along VIP card status.  If you haven't a good editor, then use your IDE to search for the step, filtering by .java file.  If you use IntelliJ, it has built in support for Gherkin so that you can put the cursor on a feature files step, then with a CTRL/CMB-B, get to the definition defined by your Java code.  If you have nothing but vanilla Eclipse (no Natural plugin), here is how to work with search:

Answering the question, "what feature files use this definition?" is a bit harder in that you need to avoid the regex expressions.  I'm not aware of any tools that help.

Adjusting only implementation
In this case, the behavior has been implemented but, darn it, the implementation just seems lacking or is in need of an update.  Assumedly the PO knows it's an update of an existing implementation by browsing through the feature documentation (maybe it's been turned into a GitBook).  The team brings the story in with a reference to the existing feature file and a simple bullet points on how to change the implementation.  During the sprint, the developers start at the feature file and from there open the definitions, read the Java code, and then make changes to the definition to get the test failing because the definition has been updated to the new implementation.  With the automation complete, the developers build the functionality (assumedly using TDD so they have micro tests which keep test automation in a sleek and pointy pyramid shape.)

Closing Thoughts

With support from a feature file editor that will escort you to the definitions, World lifecycle management enabled via dependency injection, and the fact that Cucumber is maintained by developers with significant control of the direction and vision of the company (Aslak Helles√ły,  Joseph Wilk, Matt Wynne,Gregory Hnatiuk, and Mike Sassak) I'd say give global definitions a try.  It's easy to dismiss trying something like this out of hand.  In fact some have mentioned how they've tried global definitions and failed, (see section "Global scoping is Evil").  In the case they were doing BDD incorrectly (not doing *B* DD at all in fact) as shown in the below, complaining about "click the search button."
Implementation details about UI aren't behavioral
Building "un"behavioral tests is a common early adopter's mistake which can happen to even experienced people who haven't stepped out of the box.  Games like the Behavioral or Not? teach how to build feature files at the correct level.  Building tests in the way the author of the above wanted to wasn't maintainable so this effort was in bad shape with or without global definitions.  Global definitions forced them to fail faster which was a good thing as they would give up rather than build a bunch of automation that's expensive to maintain.  This is likely one of the reasons Cucumber removed the ability to not use global definitions.  If you're still unsatisfied, use some strategies to teach Cucumber about boundaries, use a different BDD framework such as JBehave, or change Cucumber (it's open source) yourself to meet your needs.


Cucumber BDD environment installation
Global Definitions are EVIL


JUnit green bar stops rendering and tests not working

Click in the Test Selection window and look for a stack trace in the Failure Trace pane.  In cases like this, something has happened before JUnit execution has even started.  You'll need to correct the problem exposed in the Failure Trace.

Arity Problem

Failure Trace shows Arity problem
Fiddling with feature steps that already have definitions may result in the above when there is a mismatch between the number of parameters Cucumber is trying to pass from the step in the feature file into the definition.  Check the definition's parameter list and the feature file to see which needs to be straightened out.

No comments:

Post a Comment