Opinionated Cucumber

Goals

Tests should be:

  • easy to read & validate
  • predictable
  • quick to write

Solution

  • Instead of hundreds (potentially thousands) of unique but repetitive steps, have a few dozens of fully reusable steps.
     
  • The truth should not be obscured inside step implementations. Expose it by moving the truth from .js into .feature files.

Before

.then('I should see $count $type nodes?', async function(count, type) {
.then('I should see $number trees', function(count) {
.then('I should see the table for the selected assumption', async function() {
.then('I should see the commit dialog', async function() {
.then('I should see the $type message "$message"', async function(messageType, messageText) {
.then('I should see $number trees', function(count) {
.then('I should see $count assumption group(?:s|) for the $index tree', async function(count, index) {
.then('I should see $count assumption(?:s|) for the $index group of the $index tree', async function(count, index, treeIndex) {
.then('I should see a list with $count scenario-sets', function(count) {
.then('I should see the scenario-set "$name"', async function(name) {
.then('I should see an? $type element', async function(type) {
.then('I should see the info-message "$message"', function(message) {
.then('I should see $count form error(?:|s)', async function(count) {

After

.then('I should see $element', async function(element) {
.then('I should see $count $element', async function(count, element) {
.then('I $element should have text', async function(element, text) {

data-test-label="<Save>"

data-test-label="<Menu Item><About Us><Is Active>"

find('[data-test-label*="<About Us>"]')

find('[data-test-label*="<Menu Item>"][data-test-label*="<Is Active>"]')

findByLabel('<Menu Item><Is Active>')

When I type "Foo" into the <Name><Field>

When I type "Foo" into the <Name><Field> of the <Assumption Form>

When I click the 2nd <Reaction Emoji> of the 4th <Comment> inside the 1st <Tread> of the 2nd <Post>

When I type "Foo" into the 2nd <Field>

the 2nd <Field> of the 4th <Field Group>
inside the <Assumption Form>

 

 

[data-test-label*="<Assumption Form"]
[data-test-label*="<Field Group>"]:eq(4)
[data-test-label*="<Field>"]:eq(2)
.then("$element should (not )?have HTML class $text",
function (element, stateRaw, text) {
  const state = stateRaw !== "not ";

  // chai
  expect.equal(element.classList.includes(text), state);

  // chai-dom
  state
    ? expect(element).to.have.class(text)
    : expect(element).not.to.have.class(text)
})

Compact library of
100% reusable steps

  • When I click <Element>
  • When I fill field <Element> with text "These are
    not the droids you're looking for"
  • When I clear field <Element>
  • When I enable checkbox <Element>
  • When I move mouse pointer into <Element>
  • When I move mouse pointer out of <Element>
  • When I select "Yoda" in dropdown <Element>

Compact library of
100% reusable steps

  • Then I should see <Element>
  • Then I should see 2 <Element>s
  • Then <Element> should have text "Use the force, Luke!"
  • Then <Element> should have HTML class "is-active"
  • Then <Element> should have HTML attr "disabled"
  • Then <Element> should have HTML attr "aria-label"
    with value "button"
  • Then dropdown <Element> should have "Lightsaber" selected
  • Then table <Element> should be sorted by column "Rank"
    in order "asc"

Seeding

We have scenarios with:

 

  • Given there is 1 scenario-set with assumptions in my database

  • Then I should see 5 assumptions for the first group of the first tree

  • Then the first assumption should have name "Foo"

A universal approach

Given there is 1 assumption-group in my database with the following properties:
      ---
      scenarioSetId, 1
      assumptionTreeId, 1
      ---

Much better, but has issues:

  • too technical
  • too lengthy
Given a scenario set with "Id": "@scen1", "Name": "Foo"
And an assumption group with "Id": "@ass1", "Name: "Bar", "scenario": "@scen1"
And events with:
  ------------------------------------------------------------------------
  | Id    | Kind    | Message      | Parent type | Parent | Actor        |
  | @evt1 | created | It's a trap! | assumption  | @ass1  | @currentUser |
  | @evt2 | updated | Noooooooo!   | assumption  | @ass1  | @currentUser |
  ------------------------------------------------------------------------
Given events with:
  ------------------------------------------------------------------------
  | Id    | Kind    | Message      | Parent type | Parent | Actor        |
  | @evt1 | created | It's a trap! | assumption  | @ass1  | @currentUser |
  | @evt2 | updated | Noooooooo!   | assumption  | @ass1  | @currentUser |
  ------------------------------------------------------------------------
// Event
.given("events with\n$table", function (propsArrays) {

  // Iterate over table rows
  return propsArrays.map((props) => {

    // Parse according to contract
    const params = parseProps(props, {
      id:         { type: "id"},

      kind:       { type: "string" },
      message:    { type: "string" },
      
      actor:      { type: "record", model: "user" },

      parentType: { type: "string" },
      parent:     { type: "id" },
    });

    // Destructure
    let { id, kind, message, actor, parentType, parent } = params;

    // Prepare missing data
    parentType = pluralize(camelize(parentType));
    parent     = server.db[parentType].find(parent);

    // Create record
    return server.create("event", { id, kind, message, actor, parent });
  });
})
Made with Slides.com