Creating Tests for our Second Feature
Introduction
In this section, we will add a new feature around signing up to Zagat’s 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.
- In Visual Studio Code, right-click the ‘features’ folder and add a new file called ‘subscribe_to_newsletter.feature’
- Open up the ‘subscribe_to_newsletter.feature’ and add the following description of the feature along with our first test scenario, like below…
Feature: Subscribe to newsletter As Ned (a reader of email newsletters) I want to stay up-to-date with recommended restaurants So that I always have a choice of new restaurants to try out Scenario: Subscribing to the newsletter with a valid email address Given that Ned decides to use Zagat to stay updated with recommended restaurants When he tries to subscribe to the newsletter with his email address of 'ned@flanders.com' Then he is asked to also provide his zip code to accompany 'ned@flanders.com'
Adding the undefined step definitions
If we now run our tests with npm test, we will get all of our pending steps output in to the ‘pending-steps.txt’ file again.
Alternatively, if you want to run a specific test, you can install protractor globally…
npm install -g protractor
and then run the following command to specifically run the ‘subscribe_to_newsletter.feature’ file only…
protractor --specs ./features/subscribe_to_newsletter.feature
Either way, you will get pending steps output to ‘pending-steps.txt’ file like below…
Given('that Ned decides to use Zagat to stay updated with recommended restaurants', function () {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
When('he subscribes to the newsletter with his email address of {string}', function (string) {
// Write code here that turns the phrase above into concrete actions
return 'pending';
});
Then('he is asked to also provide his zip code to accompany {string}', 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, so will be naming our second step definition file based on a user role as well
- With the project open in Visual Studio Code, right-click the ‘steps’ folder and add a new file called ‘subscribers.steps.ts’
‘Given that Ned decides to use Zagat to stay updated with recommended restaurants’ step
The language of this step is similar to our step ‘Given that Frank decides to use Zagat to find recommended restaurants’.
We will want this step to essentially do the same thing and navigate to the Zagat home page. Therefore, to follow DRY (Don’t Repeat Yourself) principles, we can edit our existing ‘Given’ step in the ‘foodie.steps.ts’ file to read either way for ‘finding recommended restaurants’ or to ‘stay updated with recommended restaurants’. Let’s do this now 🙂
- Open up the ‘foodie.steps.ts’ file and edit the ‘Given’ step like so…
Given(/that (.*) decides to use Zagat to (?:find|stay updated with) recommended restaurants/, (actorName: string) => actorCalled(actorName).attemptsTo(Navigate.to('/national')) );
Unfortunately, we cannot use both Cucumber expressions and RegEx in the same step. The ‘(?:find|stay updated with)’ is RegEx that is not captured in a parameter and accepts either value on each side of the pipe. The ‘(.*)’ is RegEx that accepts any string.
The only way to use alternative text via Cucumber Expressions is via something like ‘wordOne/wordTwo’, but this does not allow multi words in the string.
We could do something like ‘(multiple words)/(alternative multi words) with Cucumber Expressions, but then either text in the brackets becomes optional, so using RegEx in this step makes more sense.
Additionally, due to how the syntax works and using RegEx now instead of Cucumber Expressions, we needed to replace the single quotes with ‘/’ at the start and end of the step
‘When he tries to subscribe to the newsletter with his email address of ned@flanders.com’ step
- Copy the pending ‘When’ step definition from the ‘pending-steps.txt’ file and add it in to the ‘subscriber.steps.ts’ step definition file, updating any necessary imports, with the whole thing looking like below…
import { When } from "cucumber"; When('he tries to subscribe to the newsletter with his email address of {string}', function (string) { // Write code here that turns the phrase above into concrete actions return 'pending'; });
- 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 { When } from "cucumber"; When('he/she tries to subscribe to the newsletter with his/her email address of {string}', (emailAddress: string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; });
‘Then he is asked to also provide his zip code to accompany ned@flanders.com’ step
- Copy the pending ‘Then’ step definition from the ‘pending-steps.txt’ file and add it in to the ‘subscriber.steps.ts’ step definition file, updating any necessary imports, with the whole thing now looking like below…
import { When, Then } from "cucumber"; When('he/she subscribes to the newsletter with his/her email address of {string}', (emailAddress: string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; }); Then('he is asked to also provide his zip code to accompany {string}', function (expectedEmailAddress: string) { // Write code here that turns the phrase above into concrete actions return 'pending'; });
- Next, edit the ‘Then’ step so we can use either he/she and use an arrow function again, with the whole thing looking like below…
import { When, Then } from "cucumber"; When('he/she subscribes to the newsletter with his/her email address of {string}', (emailAddress: string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; }); Then('he/she is asked to also provide his/her zip code to accompany {string}', (expectedEmailAddress: 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.
Defining the Step Definitions
The ‘Given’ step is already defined, so we can go straight to the next ‘When’ step 🙂
‘When he tries to subscribe to the newsletter with his email address of ned@flanders.com’ step
- Open up the ‘subscriber.steps.ts’ file and add the following import to be able to use the actor thats currently in the spotlight…
import { 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 subscribe to newsletter with a given email address
Let’s add this in to our step now
- Edit the ‘When’ step in the ‘subscriber.steps.ts’ file like below…
When('he/she tries to subscribe to the newsletter with his/her email address of {string}', (emailAddress: string) => actorInTheSpotlight().attemptsTo(SubscribeToNewsletter.with(emailAddress)) );
You will notice an error as we have not yet created our ‘SubscribeToNewsletter’ task, so let’s do that next 🙂
Adding the ‘SubscribeToNewsletter’ task
- With the project open in Visual Studio Code, right-click the ‘tasks’ folder and add a file called ‘SubscribeToNewsletter.ts’
- Open up the ‘SubscribeToNewsletter.ts’ file and add the following task to try and subscribe to the newsletter with a given email address like below…
import { Task } from '@serenity-js/core'; import { Enter, Click } from '@serenity-js/protractor'; export const SubscribeToNewsletter = { with: (emailAddress: string) => Task.where(`#actor subscribes to the newsletter with ${ emailAddress }`, Enter.theValue(emailAddress).into(ZagatNewsletterSubscriptionWidget.initialEmailField), Click.on(ZagatNewsletterSubscriptionWidget.submitEmailButton), ), };
You will notice that we get an error again as we do not have a ‘ZagatNewsletterSubscriptionWidget’ class, so again let’s implement that next 🙂
Adding the ‘ZagatNewsletterSubscriptionWidget’ class
- Right-click the ‘ui’ folder and add a new file called ‘ZagatNewsletterSubscriptionWidget.ts’
- Open up the ‘ZagatNewsletterSubscriptionWidget.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 ZagatNewsletterSubscriptionWidget { static initialEmailField = Target.the('initial email field').located(by.name('email')); static submitEmailButton = Target.the('submit email address button').located(by.className('zgt-newsletter-continue')); }
- Now let’s go back in to the ‘SubscribeToNewsletter.ts’ file and add an import for the ‘ZagatNewsletterSubscriptionWidget’ class…
import { ZagatNewsletterSubscriptionWidget } from '../ui/ZagatNewsletterSubscriptionWidget';
- Next, go back in to the ‘subscriber.steps.ts’ file and add an import for the ‘SubscribeToNewsletter.ts’ task/file…
import { SubscribeToNewsletter } from "../../spec/screenplay/tasks/SubscribeToNewsletter";
The whole thing should look similar to below at this point…
import { When, Then } from "cucumber"; import { actorInTheSpotlight } from "@serenity-js/core"; import { SubscribeToNewsletter } from "../../spec/screenplay/tasks/SubscribeToNewsletter"; When('he/she tries to subscribe to the newsletter with his/her email address of {string}', (emailAddress: string) => actorInTheSpotlight().attemptsTo(SubscribeToNewsletter.with(emailAddress)) ); Then('he/she is asked to also provide his/her zip code to accompany {string}', (expectedEmailAddress: string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; });
‘Then he is asked to also provide his zip code to accompany’ step
In this step, we will essentially be asking a question to verify that we are asked to enter a zip code to finish subscribing, through asserting we see the zip code field on the appropriate url.
Let’s add this in to our step now 🙂
- Edit the ‘Then’ step in the ‘subscriber.steps.ts’ file like below (I have added comments to explain our questions/checks)…
import { When, Then } from "cucumber"; import { actorInTheSpotlight } from "@serenity-js/core"; import { SubscribeToNewsletter } from "../../spec/screenplay/tasks/SubscribeToNewsletter"; import { Ensure, includes } from "@serenity-js/assertions"; import { Website, isVisible } from "@serenity-js/protractor"; import { ZagatNewsletterSubscriptionWidget } from "../../spec/screenplay/ui/ZagatNewsletterSubscriptionWidget"; When('he/she tries to subscribe to the newsletter with his/her email address of {string}', (emailAddress: string) => actorInTheSpotlight().attemptsTo(SubscribeToNewsletter.with(emailAddress)) ); Then('he/she is asked to also provide his/her zip code to accompany {string}', (expectedEmailAddress: string) => actorInTheSpotlight().attemptsTo( Ensure.that(Website.url(), includes('/newsletters?email='+expectedEmailAddress)), // check url we land on contains expected email address Ensure.that(ZagatNewsletterSubscriptionWidget.zipCodeField, isVisible()) // check that zip code field is displayed ) );
You will again notice that we get an error as we have not yet added the zipCodeField element, so again let’s add that next 🙂
- Open up the ‘ZagatNewsletterSubscriptionWidget.ts’ file add add locator target for the zip code field, with the whole thing looking like below…
import { Target } from "@serenity-js/protractor"; import { by } from "protractor"; export class ZagatNewsletterSubscriptionWidget { static initialEmailField = Target.the('initial email field').located(by.name('email')); static submitEmailButton = Target.the('submit email address button').located(by.className('zgt-newsletter-continue')); static zipCodeField = Target.the('zip code field').located(by.model('newslettersPageCtrl.zipcode')); }
Creating the second Cucumber Scenario
For our next test, let’s do an unhappy path scenario where Ned enters an invalid / incorrectly formatted email address.
- In Visual Studio Code, open up the ‘subscribe_to_newsletter.feature’ file again and add the following second test scenario, with the whole thing looking like below…
Feature: Subscribe to newsletter As Ned (a reader of email newsletters) I want to stay up-to-date with recommended restaurants So that I always have a choice of new restaurants to try out Scenario: Subscribing to the newsletter with a valid email address Given that Ned decides to use Zagat to stay updated with recommended restaurants When he tries to subscribe to the newsletter with his email address of 'ned@flanders.com' Then he is asked to also provide his zip code to accompany 'ned@flanders.com' Scenario: Subscribing to the newsletter with an invalid email address Given that Ned decides to use Zagat to stay updated with recommended restaurants When he tries to subscribe to the newsletter with his email address of 'nedstark' Then he is prevented by validation that contains a message about using an invalid "email address."
Creating the undefined step definition
- If we now run npm test or protractor –specs ./features/subscribe_to_newsletter.feature, the pending ‘Then’ step will be output in to the ‘pending-steps.txt’ file, like so…
Then('he is prevented by validation that contains a message about using an invalid {string}', function (string) { // Write code here that turns the phrase above into concrete actions return 'pending'; });
- Copy the pending ‘Then’ step in to the ‘subscriber.steps.ts’ file, with the whole thing now looking like below…
import { When, Then } from "cucumber"; import { actorInTheSpotlight } from "@serenity-js/core"; import { SubscribeToNewsletter } from "../../spec/screenplay/tasks/SubscribeToNewsletter"; import { Ensure, includes } from "@serenity-js/assertions"; import { Website, isVisible } from "@serenity-js/protractor"; import { ZagatNewsletterSubscriptionWidget } from "../../spec/screenplay/ui/ZagatNewsletterSubscriptionWidget"; When('he/she tries to subscribe to the newsletter with his/her email address of {string}', (emailAddress: string) => actorInTheSpotlight().attemptsTo(SubscribeToNewsletter.with(emailAddress)) ); Then('he/she is asked to also provide his/her zip code to accompany {string}', (expectedEmailAddress: string) => actorInTheSpotlight().attemptsTo( Ensure.that(Website.url(), includes('/newsletters?email='+expectedEmailAddress)), // check url we land on contains expected email address Ensure.that(ZagatNewsletterSubscriptionWidget.zipCodeField, isVisible()) // check that zip code field is displayed ) ); Then('he is prevented by validation that contains a message about using an invalid {string}', function (string) { // Write code here that turns the phrase above into concrete actions return 'pending'; });
- Next, edit the pending ‘Then’ step so it can read as he/she and uses an arrow function instead…
import { When, Then } from "cucumber"; import { actorInTheSpotlight } from "@serenity-js/core"; import { SubscribeToNewsletter } from "../../spec/screenplay/tasks/SubscribeToNewsletter"; import { Ensure, includes } from "@serenity-js/assertions"; import { Website, isVisible } from "@serenity-js/protractor"; import { ZagatNewsletterSubscriptionWidget } from "../../spec/screenplay/ui/ZagatNewsletterSubscriptionWidget"; When('he/she tries to subscribe to the newsletter with his/her email address of {string}', (emailAddress: string) => actorInTheSpotlight().attemptsTo(SubscribeToNewsletter.with(emailAddress)) ); Then('he/she is asked to also provide his/her zip code to accompany {string}', (expectedEmailAddress: string) => actorInTheSpotlight().attemptsTo( Ensure.that(Website.url(), includes('/newsletters?email='+expectedEmailAddress)), // check url we land on contains expected email address Ensure.that(ZagatNewsletterSubscriptionWidget.zipCodeField, isVisible()) // check that zip code field is displayed ) ); Then('he/she is prevented by validation that contains a message about using an invalid {string}', (expectedValidationMessage1: string) => { // Write code here that turns the phrase above into concrete actions return 'pending'; });
Defining the step definition
We will now attempt to define the remaining pending ‘Then’ step 🙂
If we try to break this step down, it could be viewed as asking a question on if the returned validation message is the same as the expected validation message.
Let’s try and implement this now.
- Edit the pending ‘Then’ step so it looks like below…
Then('he/she is prevented by validation that contains a message about using an invalid {string}', (expectedValidationMessage: string) => actorInTheSpotlight().attemptsTo(Ensure.that(EmailValidationMessage(), includes(expectedValidationMessage))) // check that returned validation message is same as expected one );
You will notice we have an error as we have not implemented the ‘EmailValidationMessage()’ const yet, so you guessed it, let’s do that next 🙂
Adding the ‘EmailValidationMessage()’ const
- Right-click the ‘screenplay’ folder and add a new file called ‘EmailValidationMessage.ts’
- Open up the ‘EmailValidationMessage.ts’ file and add the following to get the text of the ‘noPlacesFound’ element…
import { Attribute } from "@serenity-js/protractor"; import { ZagatNewsletterSubscriptionWidget } from "./ui/ZagatNewsletterSubscriptionWidget"; export const EmailValidationMessage = () => Attribute.of(ZagatNewsletterSubscriptionWidget.initialEmailField).called('validationMessage');
- Next, go back to ‘subscriber.steps.ts’ file and add the import for the ‘EmailValidationMessage()’ const…
import { EmailValidationMessage } from "../../spec/screenplay/EmailValidationMessage";
If we now run our whole test suite with npm test, all our tests should run and pass 😀