Recently at work I got the opportunity to write Behavior Driven Development tests using Cucumber.js. Cucumber uses a language called Gherkin to write the test steps and uses javascript to execute them. Gherkin is written in plain english (or a selection of other languages) and is made to be read by any (particularly non-dev) team members. If you haven't written tests before Cucumber is a great place to start.
Setting up the WebDriver
// ~/cuc-test/bdd/features/step_definitions/stepdefs.js
const { Builder, Capabilities, By } = require('selenium-webdriver')
const driver = new Builder().withCapabilities(Capabilities.chrome()).build()
Selenium WebDriver is the technology we'll use to run our tests. At a high level, it follows the test steps and executes them in a browser. Builder
creates the driver when we run our tests. Capabilities
is how you declare your browser. By
is a keyword we'll use in our tests to define what we're looking for.
I found the documentation for Selenium a bit hard to navigate. I mostly referenced the API documentation for Javascript or just searched the issue I was having.
Gherkin keywords
I spun up a simple html page with a few headers, a text input, a drop down menu, and a button to submit your changes. The first test I wrote checks that the default values are all as I'd expect them to be.
Scenario: I want to check default values of the page
Given I am on the home page
Then I should see the default title
And I should see the text input field
Scenario, Given, Then & And are all keywords that come from the gherkin syntax. In my Scenario
statement I summarize what the following test steps do. This keyword is used to group steps together and give context for users running the tests.
Given, Then & And
declare steps for a test. The string following these declaration keywords connect the step and code to execute. If you have a typo or don't match them exactly you'll receive an Undefined
error when you run the tests. This is the meat and potatoes of Cucumber testing.
Given('I am on the home page', function () {
driver.get('localhost:3000/')
})
Then('I should see the default title', function () {
driver.findElement(By.xpath(`.//*[text()[contains(.,'Welcome to the site.')]]`))
})
Then('I should see the text input field', function () {
driver.findElement(By.id('textbox'))
})
The first step of my scenario navigates to the the page I pass into the get
method. The second step tells the webdriver to search the page for the text I pass into the xpath
. The third searches for any element with an ID of textbox. Remember, we imported By
from Selenium in our set up. There a plenty of options to search by.
Scenario: I want to make sure all the colors work
Given I am on the home page
Then I want to select the color 'Green' from the drop down
And I should submit the changes
Then I should make sure the third header is 'Green'
Since I'm checking the default values in the last scenario I hardcode the parameters to search for. But it's common to pass parameters through the test steps. Notice how green is in quotations in steps two and four.
Then('I want to select the color {string} from the drop down', function (string) {
const dropDown = driver.findElement(By.id('selector'))
dropDown.click()
dropDown.sendKeys(string)
dropDown.click()
})
Then('I should make sure the third header is {string}', async function (string) {
const color = await driver.findElement(By.id('changing-header')).getCssValue('color')
// getCSSValue returns an rgb value
// colorMap is an object with keys of string and values of the associated value
expect(color).to.equal(colorMap[string])
})
Then
is a function that takes two parameters. The first parameter is the string that we used to declare the test step. Inside of that string we signal a nested variable by wrapping it in curly braces, {string}
.
The second parameter is a callback function, where we pass in the variable. Cucumber is particular about what you call the variable, if you pass in a number character you'd use int
. If you have more than one variable you'd declare it as string, string2, string3
in the callback but just as {string}
in the first parameter.
I also used a couple other methods of the driver like click()
, sendKeys(string)
, and getCssValue
. Those methods do exactly what the name implies. sendKeys
sends the string you pass in to the value field.
While working on writing a bunch of tests for old components I began to think of the tests I would write as I created new components. What types of identifiers does this component need to be testable? Is it a class name, an ID, or certain unique text that won't appear elsewhere when this is loaded? I began to think about how I would write my new components to comply with the tests I'd eventually write. In some cases, I started with the tests first. Cucumber encourages that by giving giving you the line of code you need to write when it encounters a step it doesn't recognize.
You can fork this repo and open it up on your machine if you'd like to learn by playing instead. There's a couple of tests that need to fixing in order to pass. Feel free to submit a PR if you come up with any cool tests.
Cover image: "cucumbers en route to pickledom" by Stacy Spensley is licensed with CC BY 2.0.
Top comments (0)