Cypress is a tremendous framework for creating integration tests. However, it has some limitations, such as not being able to interact with the popup windows being opened from the application under test. This might be problematic when using any external authorization provider. On the other hand, do we really need to use a real service in all of our tests? Surely we can cover the dependency between those on the different layers, while not using the actual authorization within our integration tests written in Cypress. I’d like to show you one possible approach to that problem - stubbing a third-party library.
Example overview
As an example, I will use the AccountsSDK. It not only gives you access to basic information in LiveChat accounts but also provides you with a quick way to build apps that can utilize this info.
We are particularly interested in the popup type of authorization described here: Sign in with LiveChat - popup.
Let’s say that our app uses the “Sign in with LiveChat” button. If the authorization flow succeeds, it will show the success screen next.
Here is the example component:
const accountsSDK = new AccountsSDK({
client_id: Config.lcClientId,
server_url: Config.lcAccountsURL,
})
function Login() {
const [userIdentity, setUserIdentity] = useState<UserIdentity | null>(null)
const authorize = () => {
accountsSDK
.popup()
.authorize()
.then(setUserIdentity)
}
return (
<ViewContainer>
{userIdentity === null ? (
<>
<h3>Please sign in:</h3>
<Button onClick={authorize} kind="primary">
Sign in with LiveChat
</Button>
</>
) : (
<h3>Logged in succesfully!</h3>
)}
</ViewContainer>
)
}
Thanks to the documentation, we know that “UserIdentity” resolves with the following data:
type UserIdentity = {
account_id: string
access_token: string
expires_in: number
organization_id: string
scope: string
}
So far, so good!
But when we run the Cypress tests now though, they will get stuck:
Approach overview
Testing that flow with Cypress, as mentioned above, would be a bit troublesome, as we will not be able to enter any data in the popup window. This is because the Cypress tests run “in the browser”, so it is limited to only that one particular window, and any additional ones are out of the scope.
You can find more about that here: Cypress Trade-offs. Luckily, we can make it work by using the knowledge about our application and controlling that flow pragmatically.
This can be achieved via “cy.stub()” from the Cypress API, which gives you a way to replace a function or a method. With its use, instead of the real one, the application will call the method that we provided. This is very similar to using mock functions in Jest!
In our case, we will need to hijack the “AccountsSDK” class instance so that we can provide our own fake implementation for its “popup” method.
Amending the application
Amending the application How can we take control of the AccountsSDK? We can amend our application and provide access to that object by attaching it to the “window”. This approach has been described several times by one of Cypress’ core maintainers, Gleb Bahmutov, and it even has its own dedicated blog post: Stub Objects By Passing Them Via Window Property.
Here, we would only have to modify the “Login” component so that whenever the application runs within Cypress, “accountsSDK” is accessible directly from the window:
if (window.Cypress) {
window.accountsSDK = accountsSDK
}
Now when we run the tests, “accountsSDK” will be within reach from “cy.window()” or from “cy.visit()” (which also yields the window object). The latter might come in handy whenever your application fires the authorization right away. You could then do the stubbing in “onBeforeLoad” there, to make sure you’ll hijack the object before the real one is initialized.
Stubbing the external dependency
We are now all set up for writing the tests. The flow is simple - we want to visit our application, click on the sign-up button, and check if the success screen is displayed, given the authorization was correct.
We know that the “accountsSDK” object is now attached to the window. There are built-in API methods for both accessing the “window” and stubbing that object, so we need to connect the dots. The “cy.window()” wraps the window object, therefore with the help of “cy.its()”, we should be able to shell the “accountsSDK” property. As for the next step, we will utilize “cy.then()” to provide the stub:
cy.window()
.its('accountsSDK')
.then(accountsSDK => {
/* we will stub the "popup" from the AccountsSDK here */
})
Et voilà! Side note: whenever accessing nested object properties like that, keep in mind to use retry-ability correctly. For example, in such a case like cy.window().its(‘foo’).its(‘bar’)
, only the first command is retried. You can read more about that here in the Cypress docs: Retry-ability.
Finally, the stubbing. We want to replace the “popup” method on the already-accessed accountsSDK to make sure it returns an “authorize” method. The API here looks like this: “cy.stub(object, method).returns(object)”. We already know that the “authorize()” method should return a promise with the authorization data provided by us. Here we go then:
cy.stub(accountsSDK, 'popup')
.as('popupAuthorization')
.returns({
authorize() {
return Promise.resolve({
/* authorization data object */
})
},
})
Additionally, by using “cy.as()”, we are able to provide an alias, which will allow us to later access the stub, for example, to run some assertions.
Putting it all together:
cy.window()
.its('accountsSDK')
.then(accountsSDK => {
cy.stub(accountsSDK, 'popup')
.as('popupAuthorization')
.returns({
authorize() {
return Promise.resolve({
access_token: 'token',
expires_in: 216000,
organization_id: 'org-id',
scope: 'agents--all:ro',
})
},
})
})
Keep in mind that if your application uses more than one method of the object being stubbed, you would have to stub all of them or replace the whole object.
The full test
Depending on your approach, you can either provide correct authorization data now (i.e., obtained earlier via “cy.request()”), or a fake one and then use “cy.intercept()” to take control of all the network layers of your application.
Given that we would choose the latter, the whole test will look like this:
describe('sign in', () => {
it('should show the success screen after correct authorization', () => {
cy.visit('http://localhost:3000')
cy.window()
.its('accountsSDK')
.then(accountsSDK => {
cy.stub(accountsSDK, 'popup')
.as('popupAuthorization')
.returns({
authorize() {
return Promise.resolve({
access_token: 'token',
expires_in: 216000,
organization_id: 'org-id',
scope: 'agents--all:ro',
})
},
})
})
cy.contains('Sign in with LiveChat').click()
cy.get('@popupAuthorization').should('be.calledOnce')
cy.contains('Logged in succesfully!').should('be.visible')
})
})
Thanks to “cy.stub()”, we can additionally check if the authorization was indeed correctly called only once. This was done using “cy.get()” via the alias provided earlier. Let’s run it and see how it goes:
Great! Our test is now able to access the “logged in” user flow without having to interact with the third-party popup being displayed. This would work for any other authorization library, like Google, Facebook and others.
You can find the whole repository with that example here: klarzynskik/simple-popup-auth.
Summary
As with everything, stubbing has some drawbacks as well. The flow of that authorization should certainly be covered somewhere else. However, you probably don’t want to use the real service in all of your tests, and here is where Cypress shines.
Using its API along with the knowledge of your application, you can test much quicker. Additionally, you will avoid flakiness by removing the dependency that you can’t control away from your test.
I hope you find this example useful. In case you need additional information about authorization handling in Cypress, you can follow these docs: Auth0 Authentication.