Isolating contexts for each scenario
At the moment, we are storing the request, result, error, and payload variables at the top level of the file's scope.
But step definitions can be mixed and matched in different scenarios. For example, in another scenario where we are updating a specific user, we may want to test that the API returns with the correct status code when given a malformed request. Here, we can reuse the same step definition, "our API should respond with a 400 HTTP status code", but this time, the error variable may not be set if the previous steps were defined in a different file.
Instead of using file-scoped variables, we can instead pass a context object into each step and use it to keep track of the results. This context object would be maintained throughout the entire scenario and be available in every step. In the vocabulary of Cucumber, an isolated context for each scenario is called a world. The context object is exposed inside each step as the this object.
Therefore, we can assign the response (regardless of it being a success or an error) to the more generically-named this.response and do the same for all other top-level file-scoped variables. After these changes, we should end up with the following spec/cucumber/steps/index.js file:
import superagent from 'superagent';
import { When, Then } from 'cucumber';
When('the client creates a POST request to /users', function () {
this.request = superagent('POST', 'localhost:8080/users');
});
When('attaches a generic empty payload', function () {
return undefined;
});
When('sends the request', function (callback) {
this.request
.then((response) => {
this.response = response.res;
callback();
})
.catch((error) => {
this.response = error.response;
callback();
});
});
Then('our API should respond with a 400 HTTP status code', function () {
if (this.response.statusCode !== 400) {
throw new Error();
}
});
Then('the payload of the response should be a JSON object', function () {
// Check Content-Type header
const contentType = this.response.headers['Content-Type'] || this.response.headers['content-type'];
if (!contentType || !contentType.includes('application/json')) {
throw new Error('Response not of Content-Type application/json');
}
// Check it is valid JSON
try {
this.responsePayload = JSON.parse(this.response.text);
} catch (e) {
throw new Error('Response not a valid JSON object');
}
});
Then('contains a message property which says "Payload should not be empty"', function () {
if (this.responsePayload.message !== 'Payload should not be empty') {
throw new Error();
}
});
When we refactor, we must be careful not to change the behavior of the existing code. Therefore, run our tests again to make sure they are still passing:
$ yarn run test:e2e
......
1 scenario (1 passed)
6 steps (6 passed)