Monday, May 1, 2017

Teaching Cucumber about Boundaries

Developers are trained to avoid using global anything in programs because you're making big commitments to the uncertain future.  Behavior Driven Development (BDD) demands teams and their source of requirements to decide what words mean, and then write test automation code that enforces the meaning.  In this way BDD encourages the definition of words to be used globally in a specific way in requirements.  The problem with Cumber (and all? implementations of Gherkin) is that the only information passed from the feature file to the automation framework is the the usual Gherkin keywords: Given, When, and Then, which isn't the whole picture.  Developers, since they can read the whole feature file, can teach the automation to do the right thing by linking to specific steps definitions designed with the whole situation in mind.  The BDD framework Cucumber, in particular, doesn't make it simple to control linking feature files to specific test automation (known as step definitions) because it's optimized for a flat global namespace between a feature file and the many step definitions, but there are some boundary controls.  Let's look at different levels of boundaries that can be created between your features files with Cucumber-JVM.

The Problem

Take a look at the below two feature files (click for high-res viewing):
Feature File
To implement test automation for these two feature files, a Java programmer (in the case of Cucumber-JVM) must write java code that drives their product to prove it works as the customer expects.  The problem comes up with "Then signoff" because a single Java method cannot figure out how to handle this case.  Although "signoff" is used in both features, it means two different things.  It's a pity that although the feature files have quite a lot of context (Feature title, user story, scenario title, directory location) which actually makes it clear what is meant by "signoff," none of that is available to communicate context when doing test automation in any of the Gherkin BDD frameworks.  The above two features are in two domains different domains: the domain of package delivery and the domain of multi-user systems.  One strategy is to recast the sentence "Then signoff" in one or both of those feature files so this overlap doesn't exist, and this is the first thing to try when faced with this.  In cases where such a collision is using words that are a "fixed part of the domain language" then we may want to *not* have a global namespaces and instead create a boundary (AKA a bounded context in Domain Driven Design terms).

Conceptually global step functions look like this (click pic for high resolution image):
Global Scope Step Definitions in a global 'dictionary'
Where you'll have a lot of Java steps definitions designed around each gherkin statement.  There isn't a direct relationship between a specific feature file and a specific steps definition.  Here the idea is to have many re-usable steps definitions (and an automation library that's used across all the steps definitions).


But perhaps, you want this for fine grain control and let the developer control all aspects of how each step in the feature file is automated (click pic for high resolution image):
Each feature file has it's own namespace for steps definitions
Here, each feature file gets one (or more) java classes to handle the scenarios within the feature file.  The relationship between the steps and the feature file are intimate.  The idea here is to have a very simple steps definition for a specific .feature file, and to push as much code as possible into the automation library so it can be re-used across all the steps definitions.

You can get to this extreme with some easy to do but additional steps.

Global Scope

This one is easiest to get started with as Cucumber is geared to make this easy.  Your test automation project should have package arranged like:

test.pageobjects
test.features
test.features.major_feature_one
test.features.major_feature_one.subfeature
test.features.major_feature_two
...

Create a JUnit execution class at the top of your test automation project like shown.
How everything works is that when launching the JUnit test case, it hands off to Cucumber via the @RunWith, which associates all the subpackage from that point as the namespace to use for matching step definitions.  So by putting the JUnit Test class at test.features makes all of our feature files from test.features and downward, match to any of the step definitions declared from this point on down.  (BTW, there are tools such as Natural that help in editing feature files and finding which step definition is active.)

Here's the big news: Nothing is stopping us from having multiple JUnit Test classes in different peer packages, effectively creating boundaries for how step functions are picked up by feature files.

Package Scope

By putting a Cucumberized JUnit Test case at the top of specific packages, we create separate bounded contexts, a term used by the practice Domain Driven Design, which basically sums up to: put the features and steps for the package delivery in one Java namespace and the features and steps for multiuser in another Java namespace, and then in each package, setup a Cucumberized JUnit Test case.  In this way, boundaries between these two different domains are made.

It looks like this:


Package organization
test.features.packagedeliver contains one Cucumberized JUnit Test case, many steps definitions, and many feature files.  test.features.processcontrol contains one Cucumberized JUnit Test case, many steps definitions, and many feature files.

Thusly its possible to have a "Then signoff" defined in a steps definition in the processcontrol context and a different definition for "Then signoff" by a steps definition in the packagedelivery context.  If another "Then signoff" steps definition is added to say, test.features.processcontrol.increase_system_load, Cucumber will put the brakes on and complain that "Then signoff" is ambiguous as there are multiple definitions in its bounded context.

But what if that isn't enough?  Can we make every feature file have its own context?  You bet.

Feature File Scope

If you need to do this, it's a bit of drudge work but it's not complex: for every feature file you'll need to add a Cucumberized JUnit test case, and due to a lovely quirk of Cucumber, drop that feature file's step definitions in their own package. #annoying

Conceptually that looks like this:
Literally, it looks like this (click image to see what's going on):

Two feature files in the test.features.processcontrol package want different step definitions for "Then signoff"

Now each Cucumberized JUnit test case defines the mapping from a specific feature file and a package to where to load its step definitions.  Because this strategy will end up producing a bunch of different JUnit test cases, you'll want to add a JUnit test suite so you can easily execute all your feature tests with one click.

Conclusions

You've got the tools to do whatever you want with Cucumber.  Treating every feature file as having its own scope is the ultimate of control but do you really need that?  The OCD control freak inside does want that and if so, better to find another framework to support that then Cucumber.  Defining boundaries using packages will keep the DDD people happy and would work in a pinch when you need it.  Cucumber is courageous in pushing people to build global dictionaries.  I've only heard of great drama coming out of such striving though people have successfully done it.  It seems unfair to having our language in feature files be global context (easy to do as we are humans) and then forcing the automation (a computer program) to discover the context when Gherkin doesn't passalong all the context from the feature files.

I'm happy to hear from you if you've a story to share: Twitter:@LancerKind, LancerKind@gmail.com.

References

JUnit 4 Test Suites

No comments:

Post a Comment