ExCucumber (ExCucumber v0.1.2) View Source

Basic Setup

For basic usage, kindly consult the README

Full Mastery Level

Following below represents all the various options you are empowered with in yielding this library. This has already been depicted in the README to facilitate a copy-paste-and-get-stuff-done-setup. In order to facilitate the attainment of full mastery of this library, each configuration item will be discussed in more detail.

Full Configuration

config :ex_cucumber,
  feature_dir: "#{cwd}/apps/ex_cucumber/test/support/features",
  project_root: cwd,
  # [:def, :module]
  macro_style: :module,
  # [:brief, :verbose]
  error_detail_level: :verbose,
  best_practices: %{
    disallow_gherkin_token_usage_mismatch?: false,
    enforce_context?: false,
  },

feature_dir

This represents the directory in which you embed all the documentation as .feature files together with their implementation. As this is a collaboration tool, it would not make sense to house this under the canonical test-folder. The test folder is to house your various unit tests etc., whereas the focus of this folder is the development of the ubiquitous language of your domain while holding your code base accountable in expressing the same so you may attain a 1 to 1 mapping between the conceptual framework that represents your domain and the implementation thereof.

At the time of implementing the .feature-file, you will employ the module attribute: @feature to refer to the feature file in question. For instance, say that you opt for feature_dir to point to the directory features nested in the root of your project. When you have the feature file: features/cucumber_day_and_night.feature then you could implement that in the file: features/my_amazing_implementation.exs as follows:

defmodule MyAmazingImplementation do
  use ExCucumber
  @feature "cucumber_day_and_night.feature"
end

project_root

This is to be removed soon

macro_style

This library allows you to express your verbiage in two ways:

  • Module Based: Given._ etc. Supply: :module
  • Def Based: defgiven etc. Supply: :def

error_detail_level

Cucumber has a learning curve to it; especially for junior developers of your organization. In order to accommodate every background, the ambition of this project is to provide highly detailed error messages under the format of documentation. If you feel you have a need for this, then you may supply the option: :verbose. If the pedantic amount of detail becomes disruptive to your workflow, then you may supply the option :brief instead. Kindly note that this is something I am experimenting with at the moment to strive for finding the best way of alleviating confusion.

best_practices

As the author of this library, I have my personal opinions on what constitutes best practices that may go against cucumber.io and how the developer experience has been coined in different language implementations. I incorporate such deviations into this option so you may exercise your personal preference.

best_practices.disallow_gherkin_token_usage_mismatch?

This pertains to the usage of GWT-keywords in a .feature file vs the implementation thereof. Traditionally, the people behind Cucumber were of the opinion that GWT are mere keywords that do not contribute to the meaning of your feature lines in out of themselves. As such, when it comes to matching a line in a feature file against any of the implemented cucumber expressions; their sentiment is that they should not be taken into account at all as you can read here in more detail. However, this could lead to a documentation drift where in the feature file you express a line with Given whereas in your implementation you are employing the macro defand instead. Setting the option to true means that you do not tollerate such discrepancies.

best_practices.enforce_context?

This pertains to the usage of context keywords, such as:

  • background
  • rule
  • scenario

inside your implementation as macros. It is easier to demonstrate by fleshing out the example in the README as below

defmodule MonsterFeature do
  use ExCucumber
  @feature "monster.feature"

  background do
    Given._ "there is a monster with {int} hitpoints", args do
      {
        :ok,
        %{
          monster: Monster.new(Keyword.fetch!(args.params, :int))
        }
      }
    end
  end

  scenario "Battle" do
    When._ "I attack it", args do
      attack_monster(args)
    end

    Then._ "the monster should be alive", args do
      assert args.state.monster.alive?
    end

    Then._ "it should die", args do
      assert_die(args)
    end
  end

  rule "Battle with preemptive attack" do
    background do
      Given._ "I attack the monster and do {int} points damage", args do
        massive_attack_monster(args)
      end
    end

    example "battle" do
      When._ "I attack it", args do
        attack_monster(args)
      end

      Then._ "it should die", args do
        assert_die(args)
      end
    end
  end

  rule "Battle with preemptive critical attack" do
    background do
      Given._ "I attack the monster and do {int} points damage", args do
        massive_attack_monster(args)
      end
    end

    example "battle" do
      Then._ "it should die", args do
        assert_die(args)
      end
    end
  end

  def massive_attack_monster(args), do: {:ok, %{monster: Monster.take_hit(args.state.monster, Keyword.fetch!(args.params, :int))}}
  def attack_monster(args), do: {:ok, %{monster: Monster.take_hit(args.state.monster)}}
  def assert_die(args), do: refute args.state.monster.alive?
end

When setting this option to true, then the usage of the context block-macros allows you to specify the same step definition multiple times. This is to alleviate these feature file implementations from becoming a confusing maze where repairing a broken scenario ends up in a witch hunt to locate where a particular step has been implemented.

Concretely speaking, in the corresponding feature file we can see that the step Then it should die has been reused multiple times in different contexts, nl.:

  • Scenario: Battle
  • Rule: Battle with preemptive attack/Example: battle
  • Rule: Battle with preemptive critical attack/Example: battle

Likewise there are other reused steps.

Traditionally, other cucumber implementations would limit you with only allowing to implement them once. This makes sense from the perspective that these steps should be agnostic from context as much as possible so as to encourage maximum reusability. The downside to this can be that in a very long and elaborate module, it becomes a challenge to understand the flow where you can no longer look at the implementation and suffice on it to understand how your steps are progressing. This necessitates the indirection of referring to the corresponding feature file and locating the relevant steps therein. Next is the element of finding the implemented step in all your step_definitions; e.g. the witch hunt referred to above which is exacerbated when many step definition files are imported in one and the same module. For a junior developer, this may not be contributive to developer happiness.

With this option, the implementation can fully reflect the style and nesting of the original feature file where code duplication is eliminated by means of descriptive helper functions as exemplified above.

The next step of ambition here is that this would also facilitate the localization of setup definitions. This is still a feature that is due; so be on the outlook for that!

Lastly, the only ones implemented so far are:

  • background
  • rule
  • scenario

Those three suffice to express all the rest; e.g. example can be expressed as scenario as they are synonyms. The ambition is to include distinct macros for them as well; so a PR is always welcome.

Cucumber Expressions

In the example above, you have encountered cucumber expressions in all their glory without a formal introduction to them. Make sure to take a look at the corresponding README to familiarize yourself with them.

The main exciting feature to discuss here is that of params! Instead of leveraging ugly regex to extract specific values out of feature lines, you can resort to parameters instead. There are a few that are available by standard, the canonical ones:

  • {int}
  • {float}
  • {string}
  • {word}

Here is a concrete example:

Given._ "I daily eat {int} cucumbers", args do
  assert(Keyword.fetch!(args.params, :int) == 3))
end

These params are made available in the argument that you supply in the step definition; in this case: args under the key: params as a keyword list. This would allow you to capture multiple occurrences as:

Given._ "{int} {int} {int} Here I come!", args do
  assert [int: 1, int: 2, int: 3] == args.params
end

In the case you want to introduce your own custom params, then you may formally do so by implementing the behaviour: @behaviour ExCucumber.CustomParameterType Kindly refer accordingly for more details as well as examples.

Defining Custom Parameters can be a pain. For this reason, in order to promote your happiness, you can resort to undeclared custom params.

For instance, given the following feature file:

Feature: Custom Params
Scenario:
  Given I live in New York and need to travel to Istanbul arriving before Friday, 21 July 2017
   When I input all these details into the UI
   Then I will see: Take LHRL-OSL from New York to Istanbul on Wednesday, 19 July 2017 at 13:40 to arrive by Thursday, 20 July 2017 at 09:00 for a total flight time of 17 hours at a discounted price of 2500 USD in total

You could without shame and being judged express yourself as:

Given._ "I live in {origin} and need to travel to {destination} arriving before {latest_arrival_time}", args do
  assert [origin: "New York", destination: "Istanbul", latest_arrival_time: "Friday, 21 July 2017"] == args.params
end
When._ "I input all these details into the UI", do: :ok
Then._ "I will see: Take {flight} from {origin} to {destination} on {departure_date} at {departure_time} to arrive by {arrival_date} at {arrival_time} for a total flight time of {total_flight_time} at a discounted price of {price} in total", args do
  assert [
    flight: "LHRL-OSL",
    origin: "New York",
    destination: "Istanbul",
    departure_date: "Wednesday, 19 July 2017",
    departure_time: "13:40",
    arrival_date: "Thursday, 20 July 2017",
    arrival_time: "09:00",
    total_flight_time: "17 hours",
    price: "2500 USD"
  ] == args.params
end

Of course, if you feel like cursing your existence as a software developer when having to manually parse your values time and again, then a formal introduction of one ore multiple custom parameters may be warranted instead.

Data Tables

You can access a data table occurring in a feature file, e.g.:

Given user wants to create an employee with the following attributes
| id  | firstName | lastName | dateOfBirth | startDate  | employmentType | email               |
| 100 | Rachel    | Green    | 1990-01-01  | 2018-01-01 | Permanent      | rachel.green@fs.com |

as follows:

Given._ "user wants to create an employee with the following attributes", arg, do: assert(arg.data_table)

Doc String

Analogous to the above, specify the key: doc_string in args

Scenario Outline

Full support for scenario outlines. See this implementation of this feature file

Rule

The above Monster example demonstrates full usage of Rule

Transferring state from one step to the next

When you end a step implementation with the tuple: {:ok, %{result: some_value}} then the subsequent step can access the same under args.state.result. Kindly revert to the Monster example again for practical demonstration.