Table of Contents
Creating our BDD test
Introduction
In this section, we will finally begin coding a test. The test we will perform is simply navigating to https://about.google, clicking on the ‘Our Stories’ link and verifying that we are on the correct page and see the correct elements.
Any more actions and the test would start to become less atomic. For example, if we wanted to click another button on the ‘Our Stories’ page and assert we see the correct thing based on the button that was clicked on, that would be a different test and probably one where we could skip the step where we navigate from the home page to the ‘Our Stories’ page.
Anyway, more on best practices at a later date when I start adding some articles.
Let’s begin π
Creating the 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.
- In Visual Studio Code, open up the ‘google-about.feature’ file and add the following test scenario, with the whole thing looking like below…
Feature: Google About As a Google fan, I want to find out more information about Google, so that I can learn where they came from Scenario: 01. Navigating to 'Our stories' Given a Google fan is on "https://about.google" When they navigate to "Our stories" Then they see information about Google's background story
Currently all these steps will be undefined and not have any step definitions / glue. So let’s add undefined step definitions / skeletons for them next.
Creating the Undefined Step Definitions
If we now run the following command, the console will output the pending step definitions for us…
Warnings:
1) Scenario: 01. Navigating to 'Our stories' # features\google-about.feature:7
? Given a Google fan is on "https://about.google"
Undefined. Implement with the following snippet:
Given('a Google fan is on {string}', function (string) {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
? When they navigate to "Our stories"
Undefined. Implement with the following snippet:
When('they navigate to {string}', function (string) {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
? Then they see information about Google's background story
Undefined. Implement with the following snippet:
Then('they see information about Google\'s background story', function () {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
- Open up the ‘google-about.steps.ts’ file and paste in the pending step definitions, also remembering to add in any necessary imports for the Given, When and Then’s, with the whole thing looking similar to below…
import { Given, When, Then } from "cucumber"; Given('a Google fan is on {string}', function (string) { // Write code here that turns the phrase above into concrete actions return 'pending'; }); When('they navigate to {string}', function (string) { // Write code here that turns the phrase above into concrete actions return 'pending'; }); Then('they see information about Google\'s background story', function () { // Write code here that turns the phrase above into concrete actions return 'pending'; });
- Next, we can replace the function()’s with arrow functions :), like below…
import { Given, When, Then } from "cucumber"; Given('a Google fan is on {string}', (string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; }); When('they navigate to {string}', (string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; }); Then('they see information about Google\'s background story', () => { // Write code here that turns the phrase above into concrete actions return 'pending'; });
Defining the Step Definitions
‘Given a Google fan is on {string}’
- Edit the ‘Given’ step definition so it looks like below, with the correct ‘browser’ import also being added from ‘protractor’, like below (we have also renamed the string variable to something more meaningful)…
import { browser } from "protractor"; Given('a Google fan is on {string}', (googleUrl) =>; { browser.get(googleUrl); });
‘When they navigate to ‘{string}’
For this step definition, we will be defining it by calling a function on our base-page object which performs the navigation page action for us. So first, let’s add the function for that page action π
- Open up the ‘base-page.ts’ file and edit it initially to simply be an exportable class with an empty constructor…
export class BasePage { constructor() {} }
If we go to ‘https://about.google’ in our browser (e.g. Chrome) and we inspect the ‘Our stories’ element, we can see the following…
<a href="../stories/" title="Our stories" target="_self" data-g-category="header" data-g-action="navigation" data-g-label="Our stories" data-g-module-type="Main Nav" data-g-href="/stories/"> Our stories </a>
From this we can determine that one way the element can be located is via its ‘title’ attribute, so we could find the element by using css selector…
- Let’s add a generic element finder for the navigation links…
import { ElementFinder } from "protractor"; export class BasePage { ourStoriesLink:ElementFinder; constructor() {} }
- Next, let’s set the locator for the generic ‘navLink’ element in the constructor…
import { ElementFinder, element, by } from "protractor"; export class BasePage { navLink:ElementFinder; constructor(linkTitleText:string) { this.navLink = element(by.css("a[title='"+linkTitleText+"']")); } }
- If we now go back to our step definition, we can add the following to instantiate the base page object and click the navLink we added, like so…
import { Given, When, Then } from "cucumber"; import { browser } from "protractor"; import { BasePage } from "../../pages/base-page"; let basePage:BasePage; Given('a Google fan is on {string}', (googleUrl) => { browser.get(googleUrl); }); When('they navigate to {string}', (linkText) => { basePage = new BasePage(linkText); basePage.navLink.click(); }); Then('they see information about Google\'s background story', () => { // Write code here that turns the phrase above into concrete actions return 'pending'; });
‘Then they see information about Google’s background story’
For this step definition, we will be wanting to assert that we are indeed on the ‘Our stories’ page. To keep our test atomic but also have a decent level of verification, we will include two assertions, one for verifying the url we are on and the other for verifying we the ‘View all stories’ button at the bottom of the page is displayed.
Let’s begin π
- First, in Visual Studio Code, let’s right-click the ‘pages’ folder and create a new file called ‘our-stories-page.ts’
- Next, let’s open that file up and create an exportable class that inherits from the base page and contains a constructor with a super() function setting the navLink string to ‘Our stories’, like so…
import { BasePage } from "./base-page"; export class OurStoriesPage extends BasePage { constructor() { super('Our stories'); } }
- Now, let’s add an element finder for the ‘View all stories’ button at the bottom of the ‘Our stories’ page…
import { BasePage } from "./base-page"; import { browser, ElementFinder } from "protractor"; export class OurStoriesPage extends BasePage { viewAllStoriesButton:ElementFinder; constructor() { super('Our stories'); } }
If we now inspect the ‘View all stories’ element at the bottom of the ‘Our stories’ page in the browser, we can see the following…
<a href="../all-stories/" class="view-all-stories h-c-button h-c-button--primary" target="_self"> View all stories </a>
- From the above, one way we could locate the element is by partialLinkText and checking the element contains some specific link text, so let’s find the element in our constructor…
import { BasePage } from "./base-page"; import { browser, ElementFinder, element, by } from "protractor"; export class OurStoriesPage extends BasePage { viewAllStoriesButton:ElementFinder; constructor() { super('Our stories'); this.viewAllStoriesButton = element(by.partialLinkText('View all stories')); } }
- Next, let’s go back to the step definition file and add the assertions to check both the URL and the ‘View all stories’ element is present, remembering to also instantiate the ‘our-stories-page’ class and import ‘chai’…
import { Given, When, Then } from "cucumber"; import { browser } from "protractor"; import { BasePage } from "../../pages/base-page"; import { OurStoriesPage } from "../../pages/our-stories-page"; import chai from "chai"; let expect = chai.expect; let basePage:BasePage; let ourStoriesPage:OurStoriesPage; Given('a Google fan is on {string}', (googleUrl) => { browser.get(googleUrl); }); When('they navigate to {string}', (linkText) => { basePage = new BasePage(linkText); basePage.navLink.click(); }); Then('they see information about Google\'s background story', () => { ourStoriesPage = new OurStoriesPage(); expect(ourStoriesPage.viewAllStoriesButton.isDisplayed()); });
Making things execute in order
By default, the WebDriverJS and Protractor API’s are asynchronous.
For each function that is called (e.g. from imported ‘protractor’ libraries), a promise is made that can have one of 3 states: ‘pending’, ‘accepted’ or ‘rejected’.
Regardless of the state of the promise, Javascript will by default carry on to the next line of code and execute that.
In traditional OOP languages like Java, the next line of code would only be executed after a so-called promise of the previous line has an ‘accepted’ state, making it ‘synchronous’.
Because of this, we should make our step definition functions asynchronous with the ‘async’ keyword and add ‘await”s to our appropriate lines of code, so that we can rest assure that everything gets executed in order and waits for the previous step to correctly finish.
- Update the step definitions to look like below…
import { Given, When, Then } from "cucumber"; import { browser } from "protractor"; import { BasePage } from "../../pages/base-page"; import { OurStoriesPage } from "../../pages/our-stories-page"; import chai from "chai"; let expect = chai.expect; let basePage:BasePage; let ourStoriesPage:OurStoriesPage; Given('a Google fan is on {string}', async(googleUrl) => { await browser.get(googleUrl); }); When('they navigate to {string}', async(navLink) => { basePage = new BasePage(navLink); await basePage.goToNavLink(); }); Then('they see information about Google\'s background story', async() => { ourStoriesPage = new OurStoriesPage(); await expect(ourStoriesPage.viewAllStoriesButton.isDisplayed()); });
If we now run npm test, our test should run and pass π