Creating Tests for our First Feature

Introduction

In this section, we will finally begin creating our actual test scenarios and coding them.

The website we will be testing is an Angular website called https://www.zagat.com, a US based website for national restaurant reviews and finding recommended places to eat in any given national location.

We will be essentially testing two different features.

The first feature will contain test scenarios around finding a restaurant to eat at, whilst the second feature will contain test scenarios around signing up to their email newsletter.

Let’s begin 🙂

Creating the first Cucumber Scenario

When creating our BDD scenarios, we want to keep them simple and write them in a declarative style, sticking simply to the business logic, rather than writing them in an imperative style.  A great article on this can be found at http://itsadeliverything.com/declarative-vs-imperative-gherkin-scenarios-for-cucumber.

We also want each test scenario to be small, clean and atomic. This means that ideally each test scenario should only test one specific thing and we should try to avoid repeating flows in our tests.

  1. In Visual Studio Code, open up the already created ‘find_a_restaurant.feature’ file and add the following description of the feature along with our first test scenario, like below…
    Feature: Find a restaurant
     
      As Frank (a lover of food)
      I want to find recommended restaurants to eat in
      So that I can feed my appetite with great cuisine
     
      Scenario: Searching for all restaurants in San Diego
        Given that Frank decides to use Zagat to find recommended restaurants
        When he searches for all restaurants in his area of 'San Diego'
        Then a list of recommended restaurants in 'San Diego' are returned to him

Creating the undefined step definitions

If we now run our tests with npm test we will see output like below in the console…

[19:17:27] I/launcher - Running 1 instances of WebDriver
[19:17:27] I/direct - Using ChromeDriver directly...
--------------------------------------------------------------------------------
features/find_a_restaurant.feature:7

Find a restaurant: Searching for all restaurants in San Diego

  Given that Frank decides to use Zagat to find recommended restaurants
    ☕ImplementationPendingError: Step not implemented

☕Implementation pending (26ms)

  ImplementationPendingError: Step not implemented
      at CucumberEventProtocolAdapter.outcomeFrom (/Users/t.knee/Documents/DevProjects/JavascriptProjects/TestifyQA/serenity-js-framework/node_modules/@serenity-js/cucumber/src/listeners/CucumberEventProtocolAdapter.ts:168:54)
      at EventEmitter. (/Users/t.knee/Documents/DevProjects/JavascriptProjects/TestifyQA/serenity-js-framework/node_modules/@serenity-js/cucumber/src/listeners/CucumberEventProtocolAdapter.ts:155:28)
      at EventEmitter.emit (events.js:215:7)
================================================================================
Execution Summary

Find a restaurant: 1 pending, 1 total (26ms)

Total time: 26ms
Scenarios:  1
================================================================================
[19:17:30] I/launcher - 0 instance(s) of WebDriver still running
[19:17:30] I/launcher - chrome #01 failed 1 test(s)
[19:17:30] I/launcher - overall: 1 failed spec(s)
[19:17:30] E/launcher - Process exited with error code 1
npm ERR! Test failed.  See above for more details.

In the current version of Serenity/JS, the ability to print the pending step definitions in the console has not been implemented yet (as of 03/02/2019).  You can track progress of this at https://github.com/serenity-js/serenity-js/issues/358.

I’ve spoken to Jan Molak, the creator of Serenity/JS and there’s some good bug fixes and features being added soon. If you fancy helping in the ongoing testing of Serenity/JS, please report issues at https://github.com/serenity-js/serenity-js/issues.

For now, we can use the workaround that we have already added in our ‘protractor.conf.js’ file that outputs the pending steps snippets in to a ‘pending-steps.txt’ file…

format: ['snippets:pending-steps.txt'], // workaround to get pending step definitions

If we open up our pending-steps.txt file, we will see all of our pending steps like below…

Given('that Frank decides to use Zagat to find recommended restaurants', function () {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

When('he searches for all restaurants in his area of {string}', function (string) {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

Then('a list of recommended restaurants in {string} are returned to him', function (string) {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

According to https://cucumber.io/docs/gherkin/step-organization/, step definitions can be named whatever we want, but it is generally good practice to group them by major domain objects and to avoid mapping step definition files to features, as that would be an anti pattern.

Additionally, one way we could group step definitions is by user roles. For this blog series, I have decided to take this approach, hence why we named our first step definition file ‘foodie.steps.ts

‘Given that Frank decides to use Zagat to find recommended restaurants’

  1. Copy the pending ‘Given’ step definition from the ‘pending-steps.txt’ file and add it in to the ‘foodie.steps.ts‘ step definition file, along with appropriate imports, like so…
    import { Given } from "cucumber";
    
    Given('that Frank decides to use Zagat to find recommended restaurants', function () {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
  2. Next, let’s edit the step definition a bit so that it can take any actors name as a {word} parameter by using a Cucumber expression and then also use an arrow function instead (see https://cucumber.io/docs/cucumber/cucumber-expressions/ for more info on Cucumber expressions)…
    import { Given } from "cucumber";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });

‘When he searches for all restaurants in his area of San Diego’

  1. Copy the pending ‘When’ step definition from the ‘pending-steps.txt’ file and add it in to the ‘foodie.steps.ts’ step definition file, updating any necessary imports, with the whole thing looking like below…
    import { Given, When } from "cucumber";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
    
    When('he searches for all restaurants in his area of {string}', function (string) {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
  2. Next, edit the ‘When’ step so we can use either he/she and his/her, as well as accept any location as a string parameter and use an arrow function again…
    import { Given, When } from "cucumber";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });

‘Then a list of recommended restaurants in San Diego are returned to him’

  1. Copy the pending ‘Then’ step definition from the ‘pending-steps.txt’ file and add it in to the ‘foodie.steps.ts’ step definition file, updating any necessary imports, with the whole thing looking like below…
    import { Given, When, Then } from "cucumber";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    })
    
    Then('a list of recommended restaurants in {string} are returned to him', function (string) {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
  2. Next, edit the ‘Then’ step so we can use either him/her, as well as accept any location as a string parameter and use an arrow function again…
    import { Given, When, Then } from "cucumber";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    })
    
    Then('a list of recommended restaurants in {string} are returned to him/her', (expectedLocation: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });

Please note that due to the way arrow functions work, you will need to remove the braces/curly brackets when using them.  If you instead decide to not use arrow functions, then you can keep the braces/curly brackets and add a return statement to return your step.

Setting up our actors

We will now create an exportable Actors class that returns an actor who has the ability to BrowseTheWeb.

In this blog series, we will only be interacting with a web interface, so this is fine for our needs.

In other systems, you might have multiple interfaces that need to be interacted with, such as an API layer or a messaging queue etc.

  1. With the project open in Visual Studio Code, right-click on the ‘screenplay’ folder and add a new file called ‘Actors.ts’
  2. Open up the ‘actors.ts’ file and add the following to return an actor who has the ability to BrowseTheWeb, as mentioned above, like so (as you can also see below, you could easily add multiple abilities if necessary for interacting with multiple interfaces)…
    import { Actor, Cast } from '@serenity-js/core';
    import { BrowseTheWeb } from '@serenity-js/protractor';
    import { protractor } from 'protractor';
    
    export class Actors implements Cast { // actors come from/are a part of (implement) a Cast
        prepare(actor: Actor): Actor {
            return actor.whoCan( // actors have the ability to
                BrowseTheWeb.using(protractor.browser), // browse the web using the browser object from Protractor
            );
        }
    }

Setting up our global configuration

We will now create a ‘setup.ts’ file that will contain all of our global setup and configuration.

For now, we will add a default Cucumber timeout of 10,000 milliseconds (10 seconds) as well as an ‘engage()‘ call that instantiates our Actors class 🙂

  1. With the project open in Visual Studio Code, right-click the ‘support’ folder and add a new file called ‘setup.ts’
  2. Open up the ‘setup.ts’ file and add the following so it looks like below…
    import { setDefaultTimeout } from 'cucumber';
    import { Actors } from '../../spec/screenplay/Actors';
    import { engage } from '@serenity-js/core';
    
    setDefaultTimeout(10000); // set default cucumber timeout
    engage(new Actors()); // instantiate our Actors object so they can use their given abilities

Defining the Step Definitions

‘Given that Frank decides to use Zagat to find recommended restaurants’ step

  1. Open up the ‘foodie.steps.ts’ file and add the following in the ‘Given’ step to call an actor (who has the ability to browse the web) to the stage and attempt to navigate to the Zagat (/national) home page…
    import { Given, When, Then } from "cucumber";
    import { actorCalled } from "@serenity-js/core";
    import { Navigate } from '@serenity-js/protractor';
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => 
        actorCalled(actorName).attemptsTo(Navigate.to('/national'))
    );

It is worth noting that the ‘actor.attemptsTo(..)‘ returns a promise and Serenity/JS handles a lot of the promise chaining behind-the-scenes, meaning that in a lot of cases, we don’t need to use async/await or callback()’s in order to execute our steps and lines of code in order.

‘When he searches for all restaurants in his area of San Diego’ step

Now that we have called the actor, that actor will remain in the ‘spotlight’, so we can use this actor by adding an import for ‘actorInTheSpotlight’ like so…

import { actorCalled, actorInTheSpotlight } from "@serenity-js/core";

If we break this step down in to any tasks that need to be performed, we could say that the actor in the spotlight could attempt to search for recommended restaurants in their given area of ‘San Diego’

Let’s add this in to our step now

  1. Edit the ‘When’ step in the ‘foodie.steps.ts’ file like below…
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) =>
        actorInTheSpotlight().attemptsTo(SearchForRestaurants.in(location))
    );

    You will notice that the ‘SearchForRestaurants’ task gives us an error as we haven’t created it yet, so let’s do that next

Adding the ‘SearchForRestaurants’ task
  1. With the project open in Visual Studio Code, right-click the ‘screenplay’ folder and add a new sub-folder called ‘tasks’
  2. Right-click the ‘tasks’ folder and add a file called ‘SearchForRestaurants.ts’
  3. Open up the ‘SearchForRestaurants.ts’ file and add the following task to search for recommended restaurants in a given location like below…
    import { Task } from '@serenity-js/core';
    import { Enter, Click } from '@serenity-js/protractor';
    import { Key } from 'protractor';
    
    export const SearchForRestaurants = {
        in: (location: string) =>
            Task.where(`#actor searches for recommended restaurants in ${ location }`,
                Click.on(ZagatFindAPlaceWidget.initialSearchField),
                Enter.theValue(location + Key.RETURN).into(ZagatFindAPlaceWidget.locationSearchField),
            ),
    };

    You will notice that an error is coming up as we don’t yet have a ‘ZagatFindAPlaceWidget’ class yet, so let’s implement that next 🙂

Adding the ‘ZagatFindAPlaceWidget’ class
  1. With the project still open in Visual Studio Code, right-click the ‘screenplay’ folder and add a new sub-folder called ‘ui’ that will store our UI sections (or widgets) of our web application under test
  2. Right-click the ‘ui’ folder and add a new file called ‘ZagatFindAPlaceWidget.ts’
  3. Open up the ‘ZagatFindAPlaceWidget.ts’ file and add the following so it is an exportable class that contains the two elements we need to interact with, like below…
    import { Target } from "@serenity-js/protractor";
    import { by } from "protractor";
    
    export class ZagatFindAPlaceWidget {
        static initialSearchField = Target.the('initial search field').located(by.className('zgt-header-search-text'));
        static locationSearchField = Target.the('location search field').located(by.className('zgt-search-bar-location-term-input'));
    }
  4. Now let’s go back in to the ‘SearchForRestaurants.ts’ file and add an import for the ‘ZagatFindAPlaceWidget’ class…
    import { ZagatFindAPlaceWidget } from "../ui/ZagatFindAPlaceWidget";
  5. Next, go back in to the ‘foodies.steps.ts’ file and add an import for the ‘SearchRestaurantsFor.ts’ file…
    import { SearchForRestaurants } from "../../spec/screenplay/tasks/SearchForRestaurants";

    The whole thing should look similar to below at this point…

    import { Given, When, Then } from "cucumber";
    import { actorCalled, actorInTheSpotlight } from "@serenity-js/core";
    import { Navigate } from '@serenity-js/protractor';
    import { SearchForRestaurants } from "../../spec/screenplay/tasks/SearchForRestaurants";
    
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => 
        actorCalled(actorName).attemptsTo(Navigate.to('/national'))
    );
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) =>
        actorInTheSpotlight().attemptsTo(SearchForRestaurants.in(location))
    );
      
    Then('a list of recommended restaurants in {string} are returned to him/her', (expectedLocation: string) => {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });

‘Then a list of recommended restaurants in San Diego are returned to him’ step

In this step, we will essentially be asking a question to verify that we see the correct results, through the use of a couple of assertions

Let’s add this in to our step now

  1. Edit the ‘Then’ step in the ‘foodie.steps.ts’ file like below (I have added comments to explain our questions/checks)…
    Then('a list of recommended restaurants in {string} are returned to him/her', (expectedLocation: string) => {
        const uriEncodedLocation = encodeURIComponent(expectedLocation.trim()); // replace spaces in 'expectedLocation' string with '%20'
        return actorInTheSpotlight().attemptsTo(
            Ensure.that(Website.url(), endsWith(uriEncodedLocation)), // check url we land on ends with 'San%20Diego'
            Ensure.that(ZagatPlacesWidget.returnedResults, isVisible()) // check that results are returned
        )
    });

    You will notice that an error is coming up as we don’t yet have a ‘ZagatPlacesWidget’ class yet, so let’s implement that next 🙂

Adding the ‘ZagatPlacesWidget’ class
  1. With the project still open in Visual Studio Code, right-click the ‘ui’ folder and add a new file called ‘ZagatPlacesWidget.ts’
  2. Open up the ‘ZagatPlacesWidget.ts’ file and add the following so it is an exportable class that contains the search results element we want to use, like below…
    import { Target } from "@serenity-js/protractor";
    import { by } from "protractor";
    
    export class ZagatPlacesWidget {
        static returnedResults = Target.the('search results container').located(by.className('zgt-place-results-list'));
    }
  3. Next, add an import in for the ZagatPlacesWidget class in our foodie.steps.ts file, with the whole thing looking like below now…
    import { Given, When, Then } from "cucumber";
    import { actorCalled, actorInTheSpotlight } from "@serenity-js/core";
    import { Navigate, Website, isVisible } from '@serenity-js/protractor';
    import { SearchForRestaurants } from "../../spec/screenplay/tasks/SearchForRestaurants";
    import { Ensure, endsWith } from "@serenity-js/assertions";
    import { ZagatPlacesWidget } from "../../spec/screenplay/ui/ZagatPlacesWidget";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => 
        actorCalled(actorName).attemptsTo(Navigate.to('/national'))
    );
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) =>
        actorInTheSpotlight().attemptsTo(SearchForRestaurants.in(location))
    );
      
    Then('a list of recommended restaurants in {string} are returned to him/her', (expectedLocation: string) => {
        const uriEncodedLocation = encodeURIComponent(expectedLocation.trim()); // replace spaces in 'expectedLocation' string with '%20'
        return actorInTheSpotlight().attemptsTo(
            Ensure.that(Website.url(), endsWith(uriEncodedLocation)), // check url we land on ends with 'San%20Diego'
            Ensure.that(ZagatPlacesWidget.returnedResults, isVisible()) // check that results are returned
        )
    });

Creating the second Cucumber Scenario

For our next test, let’s do an unhappy path scenario where Frank searches for all restaurants in a location that is invalid and does not actually exist

  1. In Visual Studio Code, open up the ‘find_a_restaurant.feature’ file again and add the following second test scenario, with the whole thing looking like below…
    Feature: Find a restaurant
     
      As Frank (a lover of food)
      I want to find recommended restaurants to eat in
      So that I can feed my appetite with great cuisine
     
      Scenario: Searching for all restaurants in San Diego
        Given that Frank decides to use Zagat to find recommended restaurants
        When he searches for all restaurants in his area of 'San Diego'
        Then a list of recommended restaurants in 'San Diego' are returned to him
    
      Scenario: Searching for all restaurants in a location that does not exist
        Given that Frank decides to use Zagat to find recommended restaurants
        When he searches for all restaurants in his area of 'ABCDEFG Town'
        Then no places are found

    You may notice that we have re-used our ‘Given’ and ‘When’ steps here, with only a new ‘Then’ step definition being added. Let’s add the new ‘Then’ step now 🙂

Creating the undefined step definition

If we run npm test again, we should get the pending ‘Then’ step definition output in our ‘pending-steps.txt’ file, like so…

Then('no places are found', function () {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});
  1. Copy and paste the above pending step definition in to the ‘foodie.steps.ts’ file, with the whole thing looking like below…
    import { Given, When, Then } from "cucumber";
    import { actorCalled, actorInTheSpotlight } from "@serenity-js/core";
    import { Navigate, Website, isVisible } from '@serenity-js/protractor';
    import { SearchForRestaurants } from "../../spec/screenplay/tasks/SearchForRestaurants";
    import { Ensure, endsWith } from "@serenity-js/assertions";
    import { ZagatPlacesWidget } from "../../spec/screenplay/ui/ZagatPlacesWidget";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => 
        actorCalled(actorName).attemptsTo(Navigate.to('/national'))
    );
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) =>
        actorInTheSpotlight().attemptsTo(SearchForRestaurants.in(location))
    );
      
    Then('a list of recommended restaurants in {string} are returned to him/her', (expectedLocation: string) => {
        const uriEncodedLocation = encodeURIComponent(expectedLocation.trim()); // replace spaces in 'expectedLocation' string with '%20'
        return actorInTheSpotlight().attemptsTo(
            Ensure.that(Website.url(), endsWith(uriEncodedLocation)), // check url we land on ends with 'San%20Diego'
            Ensure.that(ZagatPlacesWidget.returnedResults, isVisible()) // check that results are returned
        )
    });
    
    Then('no places are found', function () {
        // Write code here that turns the phrase above into concrete actions
        return 'pending';
    });

Defining the step definition

  1. Add the following in the pending step definition to ensure that we see the element with the text ‘no places found’, with an arrow function instead like so…
    Then('no places are found', () =>
        actorInTheSpotlight().attemptsTo(Ensure.that(NoReturnedResults(), equals('No places found')))
    );

    You will notice we get an error for ‘NoReturnedResults’, lets create an exportable constant for that now 🙂

Adding the ‘NoReturnedResults()’ const
  1. Right-click the ‘screenplay’ folder and add a new file called ‘NoReturnedResults.ts’
  2. Open up the ‘NoReturnedResults.ts’ file and add the following to get the text of the ‘noPlacesFound’ element…
    import { Text } from "@serenity-js/protractor";
    import { ZagatPlacesWidget } from "./ui/ZagatPlacesWidget";
    
    export const NoReturnedResults = () => Text.of(ZagatPlacesWidget.noPlacesFound);

    You will notice that the ‘noPlacesFound’ gives an error as the element does not exist yet, let’s add it now

  3. Open up the ‘ZagatPlacesWidget.ts’ file’ and add the following ‘noPlacesFound’ element, with the whole file now looking like below…
    import { Target } from "@serenity-js/protractor";
    import { by } from "protractor";
    
    export class ZagatPlacesWidget {
        static returnedResults = Target.the('search results container').located(by.className('zgt-place-results-list'));
        static noPlacesFound = Target.the('no places found message').located(by.className('disclaimer-headline'));
    }
  4. Finally, go back to the ‘foodie.steps.ts’ file and add in an import for the ‘NoReturnedResults’ const, like so…
    import { NoReturnedResults } from "../../spec/screenplay/NoReturnedResults";

    The whole thing should look like below now…

    import { Given, When, Then } from "cucumber";
    import { actorCalled, actorInTheSpotlight } from "@serenity-js/core";
    import { Navigate, Website, isVisible } from '@serenity-js/protractor';
    import { SearchForRestaurants } from "../../spec/screenplay/tasks/SearchForRestaurants";
    import { Ensure, endsWith, equals } from "@serenity-js/assertions";
    import { ZagatPlacesWidget } from "../../spec/screenplay/ui/ZagatPlacesWidget";
    import { NoReturnedResults } from "../../spec/screenplay/NoReturnedResults";
    
    Given('that {word} decides to use Zagat to find recommended restaurants', (actorName: string) => 
        actorCalled(actorName).attemptsTo(Navigate.to('/national'))
    );
    
    When('he/she searches for all restaurants in his/her area of {string}', (location: string) =>
        actorInTheSpotlight().attemptsTo(SearchForRestaurants.in(location))
    );
      
    Then('a list of recommended restaurants in {string} are returned to him/her', (expectedLocation: string) => {
        const uriEncodedLocation = encodeURIComponent(expectedLocation.trim()); // replace spaces in 'expectedLocation' string with '%20'
        return actorInTheSpotlight().attemptsTo(
            Ensure.that(Website.url(), endsWith(uriEncodedLocation)), // check url we land on ends with 'San%20Diego'
            Ensure.that(ZagatPlacesWidget.returnedResults, isVisible()) // check that results are returned
        )
    });
    
    Then('no places are found', () =>
        actorInTheSpotlight().attemptsTo(Ensure.that(NoReturnedResults(), equals('No places found')))
    );

If we now run npm test, it should run both our tests and they should both pass 😀

In the next section, we will start adding tests for our newsletter signup feature

Liked it? Take a second to support Thomas on Patreon!

Previous Article

Next Article

2 Replies to “Part 4. Creating Tests for our First Feature”

  1. I have everything set up exactly as you have here, but when I run npm test, i get “Frank can’t BrowseTheWeb yet. Did you give them the ability to do so?”

    And I’ve checked and double checked that I’ve followed your steps exactly, and everything looks like I did it right. I’m very lost, wondering why this is happening.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.