Scenarios using custom DSLs

One of my clients recently asked me how often I use Cucumber or JBehave in my own projects. Hardly ever, is the answer, so I want to show you what I do instead.

The English-language Gherkin syntax is hard to refactor. The tools form another layer of abstraction and maintenance on top of your usual code. There’s a learning curve that comes with them that can be a bit tricky. The only reason to use the tools is because you want to collaborate with non-technical stakeholders. If nobody outside your team is reading your scenarios after automation, then you don’t need them.

There may still be other reasons you want the tools. They’ll be more readable than the code I’m about to show you. Dynamic languages are harder to refactor anyway; I work primarily with static typing. Maybe you want to take advantage of hooks for your build pipeline. Maybe you already know and feel comfortable with the tools. Maybe you just really want to learn the technique. That’s OK. But you don’t need them.

So here’s a simple alternative.

Have some conversations, and write down the examples.

I like it when the developers do this, and get feedback on their understanding. Writing it in semi-formal Gherkin syntax is pretty useful for helping spot missing contexts and outcomes. All the usual goodness of Three Amigos conversations still applies.

Find a capability, and the thing that implements it.

Your application or system probably has a number of things that it enables people or other systems to do. We’re going to be using a noun that matches those things as a way of starting our DSL. Here are some examples:

  • Buying things -> the basket
  • Making trades -> a trade
  • Commenting on an article -> a comment / the comments
  • Doing banking -> the account

You may find the language is a bit stilted here (I did say the English was clearer!) but that’s a trade-off for the ease of getting started with this. You might find other things which make more sense to you; it’s sometimes possible to use verbs for instance.

  • Searching for a car -> I search

You’ll get the idea in a moment. Each of these is going to be the stem of a bit of code.

Start with comments in the code

Sometimes I like to just start with my scenario written in comments in the code. For each step, think about whether the step has already happened, is the thing that triggers some interesting behaviour, or is the outcome of that behaviour. Add Given, When or Then as appropriate:

    // Given an article on Climate Change
    // When I post a comment "This is a really conservative forecast."
    // Then it should appear beneath the article.

Add the Given, When or Then to your stem, and…

  • Given the basket…
  • When the trade…
  • When a comment…
  • When a search…
  • Then the account…

…construct your steps!

Now we’re in code.

GivenAnArticle().on("Climate Change")

GivenTheBasket().contains("Pack of Grey Towels")

WhenTheTrade().isCreated()
    .withCounterparty("Evil Corp")
    .forPrice(150.35, "USD")
    .....
    .andSubmitted()

WhenISearch().For("Blue Ford Fiesta")

ThenTheAccount().shouldHaveBalance(15.00, "GBP")

You can see that trading one is using a builder pattern; each step returns the trade being constructed for further changes, until it’s submitted. I sometimes like to use boring, valid defaults in my builder so that these steps only call out the really interesting bits.

I normally suggest that a “When” should be in active voice; that is, it should show who did it. If that’s important, add the actor.

WhenTheTrade().isCreated()
    .....
    .andSubmittedBy("Andy Admin")

or

WhenTheTrade().isCreated()
    .by("Andy Admin")
    .....
    .andSubmitted()

Active voice would normally look more like:

When("Andy Admin").createsATrade()
    ....
    .andSubmitsIt()

But now our “When” is ambiguous; we can’t tell which kind of capability we’re about to use, so it makes it really, really hard to maintain. It’s OK to use passive voice for DSLs.

As I construct these, I delete the comments.

Sometimes I like to just put all the detailed automation in which makes the steps run, then remove the duplication by refactoring into these steps. (Sometimes it’s enough just to just leave it with detailed automation, too, but at least leave the comments in!)

Pass the steps through to Page Objects; use the World for state

You’ll probably find you need to share state between the different steps. I normally create a “World” object, accessible from the whole scenario.

Each of the stems you created will correspond to one or more page objects. I like to keep those separate, so my steps in the DSL don’t do anything more than just call through to that object and return it.

Here’s an example of my scenario object for a Sudoku solver:

public class Scenario
{
        private readonly SudoqueSteps _sudoqueSteps;
        private readonly CellSteps _cellSteps;
        private readonly HelpSteps _helpSteps;

        private World _world;

        protected Scenario()
        {
            _world = new World();
            _sudoqueSteps = new SudoqueSteps(_world);
            _cellSteps = new CellSteps(_world);
            _helpSteps = new HelpSteps(_world);
        }
        
        protected CellSteps WhenISelectACell{ get { return _cellSteps; }}

        protected CellSteps ThenTheCell{ get { return _cellSteps; }}

        protected SudoqueSteps GivenSudoque{ get { return _sudoqueSteps; }}
        
        //...

        protected HelpSteps WhenIAskForHelp { get { return _helpSteps; } }

        protected HelpSteps ThenTheHintText { get { return _helpSteps; } }
}

It does get quite long, but it’s pretty easy to maintain because it doesn’t do anything else; all the complexity is in those underlying steps.

And here’s how I use it:

    [TestFixture]
    public class PlayerCanSetUpAPuzzle : Scenario
    {
        [Test]
        public void APlayerCanSetUpAPuzzle()
        {
            GivenSudoque.IsRunning();
            WhenISelectACell.At(3, 4).AndToggle(1);
            ThenSudoque.ShouldLookLike(
                "... ... ..." + NL +
                "... ... ..." + NL +
                "... ... ..." + NL +
                "           " + NL +
                "... ... ..." + NL +
                ".1. ... ..." + NL +
                "... ... ..." + NL +
                "           " + NL +
                "... ... ..." + NL +
                "... ... ..." + NL +
                "... ... ..." + NL);
        }
    }

Full scenarios here.

This one was written in plain old NUnit with C#. I’ve done this with JUnit and Java, and with JUnit and Kotlin. The examples here are only from toy projects, but I’ve used this technique on several real ones.

There are lots of tools out there which help you to construct these kind of DSLs; but I’ve found they also come with their own learning curve, constraints, maintainability issues etc.. This is a pretty easy thing to do; I don’t think it needs anything more complicated than I’ve put here.

It’s also very easy to refactor overly-detailed, imperative scenarios, of the kind created by lots of teams who didn’t know about the conversations, into this form.

It’s easy to move to the BDD tools if you need them.

With your Page Objects already in place, it’s pretty quick to get something like Cucumber up and running and make the step definitions call through to the page objects exactly as you were before, with just a little bit of refactoring of method names.

It’s a lot harder to move from Cucumber and regex to a DSL.

Chris Matts once had some great wisdom. “If you don’t know which technology to choose, pick the one that’s easy to change. If it’s wrong, you can change it.”

This is the one that’s easy to change, so I tend to start with this. And sometimes it doesn’t need to change.

 

 

This entry was posted in bdd. Bookmark the permalink.

13 Responses to Scenarios using custom DSLs

  1. tbeernot says:

    I like the idea of writing the cucumber scenario in the language. But somehow when starting to fiddle with that, I end up with code resembling more something like this:

    new SudokuSteps(world)
    .givenSukokuIsRunning()
    .whenISelectCell(3,4)
    .thenSukokuShouldLookLike(“….”);

    I like to logically group Cucumber steps in classes, which also applies here.

    • Liz says:

      I started that way; I found that the number of methods that ended up having to call through to the step files / page objects was unmaintainable. But it’s a fine if you don’t have too many. The common themes will emerge given enough time and code is easy to refactor!

      • tbeernot says:

        The balance of step granularity remains the same; Cucumber or like this.

      • Liz says:

        In your version, every call to the “Sudoku” stem results in another step in my “Scenario” class. That means both of these:

        givenSukokuIsRunning
        thenSukokuShouldLookLike

        But also anything I write like

        givenSudokuIsLoadedWithAPuzzle
        givenSudokuIsSaved
        givenSudokuWasSentToMyFriend(“Bob”)

        Whereas each of those would just be a call to the same stem for me:

        givenSudoku().isLoadedWithAPuzzle
        givenSudoku().isSaved
        givenSudoku().wasSentToMyFriend(“Bob”)

        The step granularity is the same; it’s the maintainability of that “Scenario” class that I’m after. But like I said, it emerges with time anyway and it’s easy to move to this if you need to, so sure, start your way. Just bear in mind that there’s another way, if you find it’s becoming a bit messy.

  2. codingnagger says:

    I have been on a project once using Cucumber to write acceptance tests. While writing the tests was easy, refactoring scenarios to make them clearer was a nightmare your method would have avoided. Definitely something to pick up for the future!

  3. Edu Costa Dev says:

    Great post! Congratulations.

    I have found more about DSL in the book Domain-Specific Languages by Martin Fowler and it is really amazing.

    I read in one of your old posts that you started to write a book.

    Did you finish?

    • Liz says:

      Not that one, but I’m working on a different series; currently on the 3rd book after which I’ll be looking for agents.

      It’s on hold at the moment so I can focus my efforts on climate change; joined a group here in London and am going to be participating in handing out leaflets, attending council meetings and holding people accountable at small scale. There’s no point writing fantasy fiction about apocalyptic scenarios when you’ve got one on your doorstep.

  4. Pingback: Five Blogs – 30 Augustus 2019 – 5blogs

  5. Pingback: Java Weekly, Issue 296 | Baeldung

  6. Henrique Sousa says:

    Hi, Liz. I am fairly new to the BDD scene, but I found it due to my explorations of Philosophy. When you talk about the human aspects of BDD, such as the Three Amigos, you get really close to proposals from people like Peter Boghossian and Daniel Kahneman, even though you don’t use the same terms. Have you gone through the work of such people?

    • Liz says:

      I haven’t read any of Boghossian’s work but Kahneman, for sure.

      Have you come across Cynefin? If not, and you like BDD from a philosophy perspective, you’re going to love Cynefin…

      • Henrique says:

        I am still working my way up, I have glimpsed Cynefin but did not dig into it yet. Is there an official site or source for that? From what I’ve heard and read, it sounds like a DSL for Socratic dialogue if that makes any sense.

      • Liz says:

        It’s a framework for making sense of different situations depending on how predictable they are. The official stuff is all over on Cognitive Edge’s website and Dave Snowden’s blog: http://cognitive-edge.com – but I wrote a little article that will give an overview: https://lizkeogh.com/cynefin-for-everyone/ . It’s related to complexity thinking (at least, the complex bit is!); the most surprising thing I learned was that you don’t need to understand an emergent situation to change it, so it’s different to Socratic dialogue in that it’s not trying to create an understanding or a definition of the situation or its aspects, just a movement. See also Cognitive Edge’s Sensemaker technology.

Leave a comment