API testing in the browser?
Yuk, why would you want to do that?
I know it sounds like a bad idea, but hear me out.
After trying many tools for integration testing of our web apps, we've found https://www.cypress.io, a great testing framework designed to be:
Fast, easy and reliable testing for anything that runs in a browser.
We liked it, and have adopted it for various needs. Its open source, developer friendly and generally a very useful tool. Its killer feature is being able to time travel, visually; to see actions as they take place and when a test fails, but also to move about in time, to the steps before a failure, which passed.
Being able to see the lead up to a failure really helps in identifying root causes, especially in the context of integration tests. It even records videos and takes screen shots if you run it headlessly (via the command line).
We were sold!
But, the UI can be slow
Even something as basic as logging in, when done via the UI, is a multi step process, requiring quite a number of clicks, page loads, characters to be typed etc. The net result of all this is a request to an API login endpoint to get a bearer token.
As suggested in the cypress tutorials, you can speed up slow multi-step UI processes by using cy.request
directly, skipping all the slow UI parts.
const options = {
method: 'POST',
url: 'http://auth.corp.com:7075/login',
qs: {
// use qs to set query string to the url that creates
// http://auth.corp.com:8080?redirectTo=http://localhost:7074/set_token
redirectTo: 'http://localhost:7074/set_token',
},
form: true, // we are submitting a regular form body
body: {
username: 'jane.lane',
password: 'password123',
},
}
// allow us to override defaults with passed in overrides
_.extend(options, overrides)
cy.request(options)
This works great.
It's a technique you can re-use in other areas, beyond login, as a way to speed up parts of a test which you are not actually testing. But, sometimes things fail - we are testing after all.
Using request like this is detailed well in the official documentation: https://docs.cypress.io/api/commands/request#Timeouts
However, API requests are 2nd class citizens in cypress. You will see what failed (the expect), as shown above, but if it fails, you won't know why/see the data.
To see the data, you need to open the console, as shown below - which is fine when working interactively, but when headless, you miss out on all this information.
Enter cypress-rest-graphql
!
Inspired by cy-api
we built cypress-rest-graphql
to make working with REST and GraphQL end points easier and importantly, to display the data even in headless mode!
The cy-api
module was good, but its focus was a little different and looked for tighter API integration / logging. We wanted something simpler, which didn't assume any API level integration. So we built cypress-rest-graphql
. (We also created a test repository, so you can see it in action).
cypress-rest-graphql
focuses on providing a few key commands:
/**
* Helper to perform GraphQL queries on the API
* Will display the query and response in the pane, for easy review.
* Automatically uses cy.env('accessToken') for auth
* @param {graphQL} query - Query or Mutation graphQL
* @param {Object} variables - Key value pairs/object used as parameters to the query
*/
cy.graphql();
/**
* Helper to periodically perform a graphQL query, checking the response against a condition.
*
* @param {graphQL} query - Query or Mutation graphQL
* @param {Object} variables - Key value pairs/object used as parameters to the query
* @param {Function} condition - Response data passed to this function, failed expects will trigger a loop.
* @param {Number} wait=100 - Millisecond delay between polling
* @param {Number} maxTries=5 - Maximum number of retries before failing totally.
*/
cy.graphQLPolling()
/**
* Wrapper to perform REST based API queries. Improved logging and review.
* @param {String} method='GET' - REST method to use
* @param {String} url - relative or absolute URL to hit
* @param {Object} postBody - Post body
*/
cy.rest()
/**
* Helper to periodically perform a rest request, checking the response against a condition.
*
* @param {String} method='GET' - REST method to use
* @param {String} url - relative or absolute URL to hit
* @param {Object} postBody - Post body
* @param {Function} condition - Response data passed to this function, failed expects will trigger a loop.
* @param {Number} wait=100 - Millisecond delay between polling
* @param {Number} maxTries=5 - Maximum number of retries before failing totally.
*/
cy.restPolling()
When these commands are called, as well as performing the request, the output is rendered to the UI, allowing you to see what may have caused an error/issue. Because it's rendered into the DOM, the time travel aspects also work really well
Please check it out, me know what you think, PRs are encouraged.
I hope you find it useful.