BDD post-mortem

Book: "The Inmates Are Running The Asylum" by Alan Cooper

  • UX should be done ahead of development
     
  • App development is a conflict of interests between users and developers
     
  • When developers get to decide the design of the app, they neglect users' problems and goals

Two things I want to improve about tests:

  • ease of implementation
  • ease of validation

How existing tests are organized

test("visiting new repository", withChai(async function (expect) {
  login();
  await page.visit({ projectId: this.project.shortName });
    
  m = "current URL";
  expect(currentURL(), m).equal("/hth/projects/platform/repositories/new");

  m = "renders form";
  expect(page.form.exists, m).true;
}));

tests/acceptance/new-repository-test.js

import { test } from "qunit";
import { withChai } from "ember-cli-chai/qunit";
import moduleForAcceptance from "sangria/tests/helpers/module-for-acceptance";
import page from "sangria/tests/pages/new-repository";

let m;

moduleForAcceptance("New Repository", {
  beforeEach() {
    this.project = server.create("project", {
      shortName: "platform",
      automaticDepotCreation: false,
    });
  },
});

test("visiting new repository", withChai(async function (expect) {
  login();
  await page.visit({ projectId: this.project.shortName });

  m = "current URL";
  expect(currentURL(), m).equal("/hth/projects/platform/repositories/new");

  m = "renders form";
  expect(page.form.exists, m).true;
}));
import { create, visitable } from "ember-cli-page-object";
import testSelector from "ember-test-selectors";

import { t } from "sangria/tests/pages/_helpers/component";
import hthSelect from "sangria/tests/pages/components/hth-select";

export default create({
  visit: visitable("/hth/projects/:projectId/repositories/new"),

  form: t("newRepository-form", {
    repositoryTypeSelect: {
      ...hthSelect,
      scope: testSelector("newRepository-repositoryTypeSelect"),
    },

/* ... */

tests/pages/new-repository.js

{{! Render `helix_path` errors here if the actual input below is hidden}}
{{#if (and repository.errors.helix_path (not isHelixGitAndHasNoAutomaticDepotCreation))}}
  {{helix-errors errors=repository.errors.helix_path}}
{{/if}}

{{#if hasEnabledRepositoryTypes}}
  <form
    data-test-newRepository-form
    {{action "submit" on="submit"}}
  >
    {{hth-select
      class="margin-bottom-15"
      data-test-newRepository-repositoryTypeSelect=true
      block=true
      content=enabledRepositoryTypes
      focusOnInsert=true
      optionLabelIconName="content.value"
      optionLabelPath="content.label"
      optionValuePath="content.value"
      triggerClass="block button"
      triggerIconName=repository.type

app/components/repository-form/template.hbs

A typical acceptance test

["project", "repository"].forEach((type) => (
  ["admin", "master", "manager", "developer", "guest"].forEach((role) => (
    test(`${type} role handling for role: ${role}`, withChai(async function (expect) {
      _.times(1, createTag.bind(this));

      const user = server.create("user", { companyAdmin: false });

      if (type === "project") {
        createProjectUserWithRole(this.project, user, role);
      } else {
        server.db.repositories.update(this.repository.id, { authorizationEnabled: true });
        createRepositoryUserWithRole(this.repository, user, role);
      }

      login(user);
      await visitRepository(this);

      // Tags rendering
      if (role === "manager" || role === "guest") {
        m = "does not render delete tag link";
        expect(page.docker.table.tags(0).deleteConfirmationTrigger.exists, m).false;
      } else {
        m = "renders delete tag link";
        expect(page.docker.table.tags(0).deleteConfirmationTrigger.exists, m).true;
      }

      // Description rendering
      if (type === "project" && role === "admin") {
        m = "renders repository description as editable";
        expect(page.docker.description.isEditable(), m).true;
      } else {
        m = "renders repository description as non-editable";
        expect(page.docker.description.isEditable(), m).false;
      }
    }))
  ))
));

My new approach to Cucumber

  "given a repository with $props"(props) {
    const params = parseProps(props, {
      id:           { regex: /@(\w+)/ },
      "short name": { regex: /`(\w+)`/ },
      "type":       { regex: /`(\w+)`/ },
      "project":    { regex: /@(\w+)/, type: "record" },
    });

    return server.create("repository", params);
  }
Feature: Repository Tree - Docker

  Background:
    Given current user is logged in
    And a project with id: @platform and short name: `platform`
    And a repository with id: @docky, project: @platform, short name: `docky` and type: `docker`
    And a tag with id: @tag-1, repository: @docky and size: 400000
    And a user with id: @jd and company admin: false


  Scenario: User with project role [Role] should see certain UI controls
    Given a project role with id: @role, project: @project and role: `[Role]`
    And a project user with project: @project, user: @jd and role: @role

    When user visits `/hth/projects/platform/repositories/docky`

    Then <Delete Tag Button> should be [Delete Button Visibility]
    And <Toggle Editing Button> in <Repository Description>  should be [Editing Button Visibility]

    Where:
      --------------------------------------------------------------------
      | Role      | Delete Button Visibility | Editing Button Visibility |
      | admin     | be visible               | be visible                | 
      | master    | be visible               | NOT be visible            |
      | manager   | NOT be visible           | NOT be visible            |
      | developer | be visible               | NOT be visible            |
      | guest     | NOT be visible           | NOT be visible            |
      --------------------------------------------------------------------


  Scenario: User with repository role [Role] should see certain UI controls
    Given a repository role with id: @role, project: @project and role: `[Role]`
    And repository @docky authorization enabled is true

    When user visits `/hth/projects/platform/repositories/docky`

    Then <Delete Tag Button> should [Delete Button Visibility]
    And <Toggle Editing Button> in <Repository Description> should NOT be visible

    Where:
      ----------------------------------------
      | Role      | Delete Button Visibility |
      | admin     | visible                  |
      | master    | visible                  |
      | manager   | NOT visible              |
      | developer | visible                  |
      | guest     | NOT visible              |
      ----------------------------------------
  {{#if isFileManagementEnabled}}
    <div class="manage-files">
      {{#confirm-inline
        isConfirming=isConfirmingDelete
        didConfirm=(action "deleteTag")
      }}
        {{hth-icon "trash-outline"
          data-test-label="<Delete Tag Button>"
          click=(toggle "isConfirmingDelete" this)
        }}
      {{/confirm-inline}}
    </div>
  {{/if}}
{{else}}
  <div class="editable-content">
    {{yield}}
    <span class="editable-actions">
      <a
        href="#" {{action "setEditing" true}}
        class="edit"
        data-test-label="<Toggle Editing Button>"
      >
        {{hth-icon "edit-alt"}}
      </a>
    </span>
  </div>
{{/if}}

Benefits

  • Create a new test by simply writing a user story
     
  • Can copy-paste steps from existing user stories
     
  • Surprisingly versatile and full-featured
     
  • Exposes the truth directly in user story text
     
  • Does not prevent from using the old approach when absolutely necessary

Cucumber Recap

By Andrey Mikhaylov (lolmaus)