Testing an Akita-Angular Application with Cypress

Max Baumann

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.

cypressBinding.ts
export function queryCypressBinding(query) {
    if (window.Cypress) { ...  } else { ... }
}
app.query.ts
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.

cypressBinding.ts
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];
    }
}
app.service.ts
export class AppService {
  constructor(private store: AppStore, private query: AppQuery) {
    serviceCypressBinding(this);
  }
}

Put this to use like this:

anothertest.js
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
});