Testing an Akita-Angular Application with Cypress
Cypress is one of the easiest ways to test your Angular application. But because it is not tied to any Angular API it is hard to look "under the hood" of your tested app. However directly manipulating the internal State of it can make testing even easier. This post will show you a way to achieve this.
Unfortunately, we need to write a bit of overhead into our Application, this is marginal though.
Get State in Cypress
To write a binding for Cypress we need to create a function that needs to be called in the constructor of each of our Akita Queries. Make sure to pass the query itself to it using this
. Cypress provides a global window.Cypress
variable we can use todetermine whether we are in a Cypress testing environment.
export function queryCypressBinding(query) {
if (window.Cypress) { ... } else { ... }
}
export class AppQuery extends Query<AppState> {
constructor(protected store: AppStore) {
super(store);
queryCypressBinding(this); // <-- Add this line to every Query
}
}
Our goal is to provide a field that allows access from Cypress. I decided to use the Class Name for that. Every time the State changes this field should get updated. We can do this the Akita way using query.select()
which will listen for every state-change.
export function queryCypressBinding(query) {
const name = query.constructor.name; // e.g. AppQuery
// @ts-ignore
if (window.Cypress) {
// @ts-ignore
query.select().subscribe(_ => window[name] = query.getValue()); // updates the field with new state
} else {
delete window[name]; // to make sure we dont leak state in production
}
}
Nice! Using this we can test our state in Cypress like this:
sometest.js
:
it('should to sth', () => {
cy.visit('http://localhost:4200/');
// do stuff
cy
.window() // get app's window variable
.its('AppQuery') // get store
.its('somevalue') // this depends on your store
.should('exist') // do whatever testing you want here
});
Manipulate state in Cypress
We now have read-access to our state. But how can we dispatch actions from our testing suite? You might have guessed it, we expose our service to Cypress. So let's write another function to do so and call it in every constructor of our services.
export function serviceCypressBinding(service) {
const name = service.constructor.name;
// @ts-ignore
if (window.Cypress) {
console.log('testing environment detected adding ' + name);
// @ts-ignore
window[name] = service;
} else {
delete window[name];
}
}
export class AppService {
constructor(private store: AppStore, private query: AppQuery) {
serviceCypressBinding(this);
}
}
Put this to use like this:
it('should manipulate stuff', () => {
cy
.window() // get app's window variable
.its('AppService')
.invoke('update', {
somevalue: 'Hello World'
});
// obvserve changes
});
or call a function on your Service:
it('should manipulate more stuff', () => {
cy
.window() // get app's window variable
.its('AppService')
.invoke('removeAllTodos'); // call your function
// obvserve changes
});