Testing NgRx Store with Cypress Component Testing
Published: 2024-01-05 22:07:02

## What is NgRx 
NgRx has been one of my most favorite open-source projects I've worked on in my career. It provides several mechanisms for managing reactivity in Angular applications. Most commonly in it's `@ngrx/store` package in combination with `@ngrx/effects`. To understand what it does at a high level I like to refer to the diagram below which I created several years back. 

[![NgRx Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gei3m56eba9d51a9gi0s.png)](https://ngrx.io/guide/store)

This article will not go into the weeds of NgRx but will focus on integrating it with Cypress Component Testing. However, there are endless resources online for learning how to get started with NgRx. I recommend visiting their [Getting Started Docs](https://ngrx.io/guide/store) if you want to learn more or are looking for a quick refresher. 

## Cypress Component Testing
For those of you not familiar with Cypress Component Testing, it provides a component workbench for you to quickly build and test components from multiple front-end UI libraries — no matter how simple or complex. 

> "I like to say it's like Jest or Karma had a baby with Storybook!"

What I love about it personally is how simple it is to create really meaningful tests! If you are new to Component Testing I recommend watching my video below showing how to get starting with Component Testing in Angular or you can visit the [Angular Component Testing Cypress Docs](https://docs.cypress.io/guides/component-testing/angular/overview).

{% embed https://www.youtube.com/watch?v=weWTHGWe6uU %}
 
## Creating Our Store
Let's use a simple example of a counter that both increments and decrements a count and then displays the total count as a number in a `StepperComponent`. To get started we always want to start with our actions!

```ts
// src/app/store/count.actions.ts

import { createAction } from '@ngrx/store' 

export const incrementCount = createAction('[COUNT] increment count')
export const decrementCount = createAction('[COUNT] decrement count')
export const clearCount = createAction('[COUNT] clear count')
```

Now let's create a reducer to handle our actions:

```ts
// src/app/store/count.reducer.ts

import { createFeature, createReducer, on } from '@ngrx/store' 
import { incrementCount, decrementCount, clearCount } from './count.actions'

interface State {
  count: number
}

const initialState: State = {
  count: 0,
}

const countFeature = createFeature({
  name: 'count',
  reducer: createReducer(
    initialState,
    on(incrementCount, ({ count }) => ({
      count: count + 1
    })),
    on(decrementCount, ({ count }) => ({
      count: count - 1
    })),
    on(clearCount, () => ({
      count: 0
    }))
  )
})

export const { name: reducer, selectCountState, selectCount } = countFeature
```

Now let's create a `StepperComponent` that will allow users to increment, decrement and clear the count by integrating it with our store.

```ts
// src/app/stepper/stepper.component.ts

import { Component, inject } from '@angular/core' 
import { Store } from '@ngrx/store' 
import { selectCount } from '../store/count.reducer' 
import { incrementCount, decrementCount, clearCount } from '../store/count.actions

@Component({
  selector: 'app-stepper',
  template: `
    <button (click)="decrement()">-</button>
    <span>{{ count$ | async }}</span>
    <button (click)="increment()">+</button>
    <br /><br />
    <button (click)="clear()">Clear</button>
  `,
  styleUrls: ['./stepper.component.css'],
})
export class StepperComponent {
  private readonly store = inject(Store)

  count$ = this.store.select(selectCount)

  increment() {
    this.store.dispatch(incrementCount())
  }

  decrement() {
    this.store.dispatch(decrementCount())
  }

  clear() {
    this.store.dispatch(clearCount())
  }
}
```

Now let's import our StoreModule into our App.Module:

```ts
// src/app/app.module.ts

import { StoreModule } from '@ngrx/store' 
import { reducer } from './store/count.reducer'
...

@NgModule({ 
  ...
  imports: [
    ...,
    StoreModule.forRoot({
      count: reducer
    })
  ],
})
export class AppModule {}
```

**Note**
_You can apply the same concepts for Standalone Components as well_ [https://ngrx.io/guide/store/reducers#standalone-api-in-module-based-apps](https://ngrx.io/guide/store/reducers#standalone-api-in-module-based-apps)


## Writing our First Test
Now that we have our Store connected to our new Stepper Component, lets install cypress (if you haven't already done so) by running the following bash command:

```bash
npm install cypress@latest
```

Now let's write our first test by creating a new file next to our `StepperComponent` named `stepper.component.cy.ts`

```ts
// src/app/stepper/stepper.component.cy.ts

import { StepperComponent } from './stepper.component'

describe('StepperComponent', () => {
  it('can mount', () => {
    cy.mount(StepperComponent)
  })
})
```

Now let's launch Cypress and click on our new spec file:

```bash
npx cypress open --component
```

Though our test was super easy to write we unfortunately get the following error:

![Failure State without Providing Store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6rbtgzcqoea8dklsgbwv.png)

This is because we haven't configured our test with our Store. Don't worry this is actually super easy to do! Because we configured our `AppModule` with our store we can either do one of two things:

1. We can copy the Store configuration from our `AppModule` into our `mount`
2. We can just import `AppModule` into our `mount`

Let's do the later as it requires less code:

```ts
// src/app/stepper/stepper.component.cy.ts

import { StepperComponent } from './stepper.component'
import { AppModule } from '../app.module'

describe('StepperComponent', () => {
  it('can mount', () => {
    cy.mount(StepperComponent, {
      imports: [AppModule]
    })
  })
})
```

And just like that our `StepperComponent` connected to our store is mounting successfully in our Cypress Component Test!

![Passing First Test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ikzg4bhpomyqsj8bxe1j.png)

Because we will most likely need our Store in every component we mount I recommend doing the following customization to your `mount` command:

```ts
// cypress/support/component.ts

import { mount } from 'cypress/angular'
import { AppModule } from 'src/app/app.module'

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount
    }
  }
}

// This is necessary as mount takes a generic for it's first two arguments
type MountParams = Parameters<typeof mount>

Cypress.Commands.add(
  'mount',
  (component: MountParams[0], config: MountParams[1] = {}) => {
    return mount(component, {
      ...config,
      imports: [
        ...(config.imports || []),
        AppModule
      ]
    }
  }
)
```

Now every time we call `cy.mount` we are automatically importing `AppModule`. Now we can change our test back to how it was originally and we should get a successfully mounted Stepper Component.

```ts
describe('StepperComponent', () => {
  it('can mount', () => {
    cy.mount(StepperComponent)
  })
})
```

## Beyond The Basics
**Selectors**
Though we have our component mounted we aren't running any assertions against it. Let's add a test validating that the `count$` observable which is using our `selectCount` selector is working correctly by asserting the initial value of count is zero.

```ts
  it('has a correct default count of 0', () => {
    cy.mount(StepperComponent);
    cy.get('span').should('have.text', '0');
  });
```

Now let's test incrementing the count:

```ts
  it('can increment the count', () => {
    cy.mount(StepperComponent);
    cy.get('button').contains('+').click();
    cy.get('span').should('have.text', '1');
  });
```

And then decrementing the count:

```ts
  it('can decrement the count', () => {
    cy.mount(StepperComponent);
    cy.get('button').contains('-').click();
    cy.get('span').should('have.text', '-1');
  });
```

Finally let's test clearing the count:

```ts
  it('can clear the count', () => {
    cy.mount(StepperComponent);
    cy.get('button').contains('+').click().click();
    cy.get('span').should('have.text', '2');
    cy.get('button').contains('Clear').click();
    cy.get('span').should('have.text', '0');
  });
```

Now we should have 5 passing tests that are validating the various ways a user might interact with our Stepper Component.
![Basic Passing Tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4axotcr9jo82k0cwzc9e.png)

**Actions**
Though we know our component appears to be working as expected, in less trivial use-cases we will want to test our component's integration with our store more thoroughly. Let's dive deeper into dispatched actions.

There are several ways of approaching writing tests for dispatching actions. Let me first start off with the less ideal implementation:

If we look at our Component we see that we are using Angular's `click()` [event binding](https://angular.io/guide/event-binding) to call a public method in our component's class that eventually calls our store's dispatch method.

For example this button when clicked calls the `increment()` method in our component class:

```html
<button (click)="increment()">+</button>
```

Then our component class increment method dispatches an `incrementCount()` action to our store:

```ts
private readonly store = inject(Store);

increment() {
  this.store.dispatch(incrementCount());
}
```

Because the increment method is a public property in our class we can easily write a test that asserts that it is called when the increment button is clicked:

```ts
  it('can spy on increment invocation', () => {
    cy.mount(StepperComponent).then(({ component }) => {
      cy.spy(component, 'increment').as('increment');
    });
    cy.get('button').contains('+').click();
    cy.get('@increment').should('have.been.calledOnce');
    cy.get('span').should('have.text', 1);
  });
```

We can then do the same thing for `decrement`, `clear`, etc.

Though this does validate that our button click is binding correctly to our methods it doesn't actually do any validating that a specific action is being dispatched. Let's try to take this same approach but apply it to our store's dispatch method.

```ts
  it('can spy on store.dispatch', () => {
    cy.mount(StepperComponent).then(({ component }) => {
      cy.spy(component.store, 'dispatch').as('dispatchSpy');
    });
    cy.get('button').contains('+').click();
    cy.get('@dispatchSpy').should('have.been.called');
  });
```

If you haven't already identified the issue with this test you should see the following error because our `store` property is a private field in our class.

![Store Private Property Error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lmvnphi7atjjaf88hfry.png)

Though we are getting this error, the test is actually correct and will pass when we run it. Let's manually swallow this TS error by adding a [ts-expect-error comment](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#-ts-expect-error-comments)

```ts
...
cy.mount(StepperComponent).then(({ component }) => {
  // @ts-expect-error
  cy.spy(component.store, 'dispatch').as('dispatchSpy');
});
...
```

We are now in business and can write tests validating specific actions are dispatched to our NgRx store. But let's make this even easier by writing a few custom commands!

### Custom NgRx Store Commands
**cy.store()**
We first need an easy way to access our store since it is private in our components. Let's create a new [custom command](https://docs.cypress.io/api/cypress-api/custom-commands#__docusaurus_skipToContent_fallback):

```ts
// cypress/support/component.ts

import { Store } from '@ngrx/store' 

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount;
      store(storePropertyName: string): Cypress.Chainable<Store>;
    }
  }
}

Cypress.Commands.add(
  'store',
  { prevSubject: true },
  (subject: MountResponse<MountParams>, storePropertyName: string) => {
    const { component } = subject;

    // @ts-expect-error
    const store = component[storePropertyName] as Store;

    return cy.wrap(store);
  }
);
```

Now let's create a test using our new `store` command:

```ts
  it('can use cy.store()', () => {
    cy.mount(StepperComponent)
      .store('store')
      .then((store) => {
        cy.spy(store, 'dispatch').as('dispatchSpy');
      });

    cy.get('button').contains('+').click();
    cy.get('@dispatchSpy').should('have.been.called');
    cy.get('span').should('have.text', 1);
  });
```

Now we can easily access our store by just chaining off our our `cy.mount()` command. But let's go one step further and make spying on our dispatch easier by creating a `dispatch` command:

**cy.dispatch()**

```ts
// cypress/support/component.ts

import { Store } from '@ngrx/store' 

declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount;
      store(storePropertyName: string): Cypress.Chainable<Store>;
      dispatch(): Cypress.Chainable;
    }
  }
}

Cypress.Commands.add('dispatch', { prevSubject: true }, (store: Store) => {
  return cy.wrap(cy.spy(store, 'dispatch').as('dispatch'));
});
```

Now we can write a new test using both of our new commands:

```ts
  it('can use cy.dispatch()', () => {
    cy.mount(StepperComponent).store('store').dispatch();
    cy.get('button').contains('+').click();
    cy.get('@dispatch').should('have.been.called');
    cy.get('span').should('have.text', 1);
  });
```

TADA! We now can easily write Cypress Component Tests for our Components that are tied to an NgRx Store!


![Finished Tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljmgicqqvlsiifhliszx.png)


## Conclusion:
Using Component Tests to write tests for your Angular Component's using NgRx Store is both super easy and creates high value tests. This article certainly doesn't cover everything that can be tested like this but hopefully paints a clear enough picture so you can see the power of using Cypress Component Testing with NgRx.