DEV Community

Cover image for How to mock NgRx Signal Stores for unit tests and Storybook Play interaction tests (both manually and automatically)
Gergely Szerovay for This is Angular

Posted on • Originally published at angularaddicts.com

How to mock NgRx Signal Stores for unit tests and Storybook Play interaction tests (both manually and automatically)

In this article, I show you two techniques for mocking Signal Stores: 

  • creating mock Signal Stores manually, and
  • using the provideMockSignalStore function (it generates a mock version of a Signal Store) that’s designed to simplify the mocking process

Both of these techniques convert signals to writable signals, substitute functions with Sinon fakes, and replace RxMethods with their fake counterparts in the Signal Store. These techniques together with ng-mocks streamline the testing setup, enabling you to mock component dependencies (like services, stores, and child UI components) efficiently. I also cover how to apply this mocking approach in case of using custom store features or Storybook Play interaction tests.

So I'm going to show you how to unit test a component that uses a Signal Store using mock stores, and also an example on how to create a Storybook Story with a Storybook Play interaction test for the same component (again, using a mock store).

Source code and demo app

The full source code of the mock signal store provider is available here: provideMockSignalStore

Demo app source:

Prerequisites

To get the most out of this article, you should have a basic understanding of how Signals and Signal Store work:

Angular Signals is a new reactivity model introduced in Angular 16. The Signals feature helps us track state changes in our applications and triggers optimized template rendering updates. If you are new to Signals, here are some highly recommended articles as a starting point:

The NgRx team and Marko Stanimirović created a signal-based state management solution, SignalStore. If you are new to Signal Stores, you should read Manfred Steyer's four-part series about the NgRx Signal Store:

It's also important to understand how RxMethods work.

Article list component

In the demo app, we have the ArticleListComponent_SS component. It's a smart component and has a component level store, ArticleListSignalStore.

@Component({
  selector: 'app-article-list-ss',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [UiArticleListComponent, UiPaginationComponent, HttpRequestStateErrorPipe],
  providers: [ArticleListSignalStore],
  templateUrl: 'article-list-signal-store.component.html',
})
export class ArticleListComponent_SS {
  // we get these from the router, as we use withComponentInputBinding()
  selectedPage = input<string | undefined>(undefined);
  pageSize = input<string | undefined>(undefined);

  HttpRequestStates = HttpRequestStates;

  readonly store = inject(ArticleListSignalStore);

  constructor() {
    LogSignalStoreState('ArticleListSignalStore', this.store);
    effect(() => {
      // 1️⃣ the effect() tracks these two signals only
      const selectedPage = this.selectedPage();
      const pageSize = this.pageSize();
      // 2️⃣ we wrap the function we want to execute on signal change
      // with an untracked() function
      untracked(() => {
        // we don't want to track anything in this block
        this.store.setSelectedPage(selectedPage);
        this.store.setPageSize(pageSize);
        this.store.loadArticles();
      });
      console.log('router inputs ➡️ store (effect)', selectedPage, pageSize);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode
<h1 class="text-xl font-semibold my-4">SignalStore</h1>
<!-- 👇 Main UI state: initial / fetching 📡 -->
@if (
  store.httpRequestState() === HttpRequestStates.INITIAL ||
  store.httpRequestState() === HttpRequestStates.FETCHING
) {
  <div>Loading...</div>
}
<!-- 👇 Main UI state: fetched 📡 -->
@if (store.httpRequestState() === HttpRequestStates.FETCHED) {
  <!-- 👇 Article list UI component -->
  <app-ui-article-list [articles]="store.articles()" />
  <!-- 👇 Pagination UI component -->
  <app-ui-pagination
    [selectedPage]="store.pagination().selectedPage"
    [totalPages]="store.pagination().totalPages"
    (onPageSelected)="store.setSelectedPage($event); store.loadArticles()"
  />
}
<!-- 👇 Main UI state: error 📡 -->
@if (store.httpRequestState() | httpRequestStateErrorPipe; as errorMessage) {
  {{ errorMessage }}
}
Enter fullscreen mode Exit fullscreen mode

The component has two signal inputs, it gets these from the router (we use withComponentInputBinding()):

  • selectedPage
  • pageSize

The component renders the following dumb/UI components, based on the state in the store:

  • an article list component, and
  • a pagination component

The component updates the state in the Store, if one of the following things happen:

  • the selectedPage or pageSize values change in the URL, or
  • the user selects another page using the pagination component

Signal Store for the Article list component

The store has the following state (source code):

export type ArticleListState = {
  readonly selectedPage: number,
  readonly pageSize: number,

  readonly httpRequestState: HttpRequestState,

  readonly articles: Articles,
  readonly articlesCount: number
}
Enter fullscreen mode Exit fullscreen mode

And this is the Signal Store itself (source code):

export const ArticleListSignalStore = signalStore(
  withState<ArticleListState>(initialArticleListState),
  withComputed(({ articlesCount, pageSize }) => ({
    totalPages: computed(() => Math.ceil(articlesCount() / pageSize())),
  })),
  withComputed(({ selectedPage, totalPages }) => ({
    pagination: computed(() => ({ selectedPage: selectedPage(), totalPages: totalPages() })),
  })),
  withMethods((store) => ({
    setSelectedPage(selectedPage: string | number | undefined): void {
      patchState(...);
    },
    setPageSize(pageSize: string | number | undefined): void {
      patchState(...);
    },
    setRequestStateLoading(): void {
      patchState(...);
    },
    setRequestStateSuccess(params: ArticlesResponseType): void {
      patchState(...);
    },
    setRequestStateError(error: string): void {
      patchState(...);
    },
  })),
  withMethods((store, articlesService = inject(ArticlesService)) => ({
    loadArticles: rxMethod<void>(
      pipe(...),
  })),
);
Enter fullscreen mode Exit fullscreen mode

The store has the following state signals: selectedPage: number, pageSize: number, httpRequestState: HttpRequestState, articles: Articles, articlesCount: number.

It also has:

  • computed selectors: totalPages and withComputed
  • methods for updating its state: setSelectedPage, setPageSize, setRequestStateLoading, setRequestStateSuccess, setRequestStateError, and
  • an rxMethod for fetching the article list from the ArticlesService: loadArticles

Mocking Signal Stores

Let’s say we want to cover a smart component with unit tests, and the component contains complex logic, for example connections between app-, feature- and component level stores.

In this case, it can be useful to mock for instance services, stores, child components, all the things the component relies on.

I use the MockComponent() and MockProvider() functions from ng-mocks for mocking components and plain services.

MockProvider() doesn't work well with NgRx's ComponentStores and SignalStores, as it doesn't support ComponentStore's update() and effect() function, nor does it support RxMethods. So we create a custom mock version of ArticleListSignalStore. In order to do that, we replace:

  • Signals by WritableSignals
  • Functions by Sinon fakes
  • RxMethods by FakeRxMethods

This way we can explicitly set the selector signals' values in the unit test, and also check whether the functions and RxMethods were called and with what parameters.

FakeRxMethods are generated by the newFakeRxMethod() function (source code). A FakeRxMethod is a function that accepts a static value, signal, or an observable as an input argument. It has a FAKE_RX_METHOD property, and contains a Sinon fake. This Sinon fake stores the call information in the following cases: If the FakeRxMethod was called

  • with a static value
  • with a signal argument, and the signal's value changes
  • with an observable argument, and the observable emits

Here is a TestBed for the article list component (source code), as you’ll see, the component's Signal Store and all the child UI components are mocked:

class MockArticleListSignalStore {
  // Signals are replaced by WritableSignals
  selectedPage = signal(0);
  pageSize = signal(3);
  httpRequestState = signal<HttpRequestState>(HttpRequestStates.INITIAL);
  articles = signal<Articles>([]);
  articlesCount = signal(0);

  // Computed Signals are replaced by WritableSignals
  totalPages = signal(0);
  pagination = signal({ selectedPage: 0, totalPages: 0 });

  // Functions are replaced by Sinon fakes
  setSelectedPage = sinon.fake();
  setPageSize = sinon.fake();
  setRequestStateLoading = sinon.fake();
  setRequestStateSuccess = sinon.fake();
  setRequestStateError = sinon.fake();

  // RxMethods are replaced by FakeRxMethods
  loadArticles = newFakeRxMethod();
}

describe('ArticleListComponent_SS - mockComputedSignals: true + mock all child components', () => {
  let component: ArticleListComponent_SS;
  let fixture: ComponentFixture<ArticleListComponent_SS>;
  // we have to use UnwrapProvider<T> to get the real type of a SignalStore
  let store: UnwrapProvider<typeof ArticleListSignalStore>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        ArticleListComponent_SS,
        MockComponent(UiArticleListComponent),
        MockComponent(UiPaginationComponent),
      ],
      providers: [],
    })
      .overrideComponent(ArticleListComponent_SS, {
        set: {
          providers: [
            // override the component level providers
            MockProvider(ArticlesService), // injected in ArticleListSignalStore
            {
              provide: ArticleListSignalStore,
              useClass: MockArticleListSignalStore,
            },
          ],
        },
      })
      .compileComponents();

    fixture = TestBed.createComponent(ArticleListComponent_SS);
    component = fixture.componentInstance;
    // access to a service provided on the component level
    store = fixture.debugElement.injector.get(ArticleListSignalStore);
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});  
Enter fullscreen mode Exit fullscreen mode

By using this approach, we manually create the MockArticleListSignalStore, and we have to update it each time we change the structure of the ArticleListSignalStore.

We get the type of the Signal Store with the UnwrapProvider<typeof ArticleListSignalStore>, as the signalStore() function returns a provider, not the store itself.

We use the overrideComponent() function to override the component level providers of the article list component. We can get a store provided on the component level by store = fixture.debugElement.injector.get(ArticleListSignalStore);. This store is the mocked version of the original store.

Now it's time to write some unit tests.

In the constructor() of the article list, we have an effect() that gets the selectedPage() and pageSize() signals from the router, updates the store by their values then calls the store.loadArticles() to fetch the articles from the server. So after we created the component in the testBed and ran the change detection with the detectChanges(), the effect and the loadArticles() should be executed:

  describe('router inputs ➡️ store (effect)', () => {
    it("should update the store's state initially", () => {
      expect(getRxMethodFake(store.loadArticles).callCount).toBe(1);
    });
  });
Enter fullscreen mode Exit fullscreen mode

store.loadArticles is a FakeRxMethod, and the getRxMethodFake() function returns the Sinon fake that stores the call information for FakeRxMethod.

We can also verify, that the effect runs store.loadArticles() if the selectedPage() input changes:

  describe('router inputs ➡️ store (effect)', () => {
    it('should call loadArticles if the selectedPage router input changes', () => {
      getRxMethodFake(store.loadArticles).resetHistory();
      fixture.componentRef.setInput('selectedPage', '22');
      fixture.detectChanges(); // run the change detection to re-evaluate effects
      expect(getRxMethodFake(store.loadArticles).callCount).toBe(1);
    });
  });
Enter fullscreen mode Exit fullscreen mode

We use componentRef.setInput() to change the component's input, as this method supports signal inputs, too. Then we run the detectChanges() to trigger the change detection that re-evaluates the effects. Finally, we expect that store.loadArticles() was called.

In the following test, we simulate a situation in which the article list is loaded, and we check that the article list is rendered and it gets the articles from the store:

    describe('Main UI state: FETCHED', () => {
      let uiPaginationComponent: UiPaginationComponent;
      let uiArticleListComponent: UiArticleListComponent;
      beforeEach(() => {
        asWritableSignal(store.httpRequestState).set(HttpRequestStates.FETCHED);
        asWritableSignal(store.articles).set([
          { slug: 'slug 1', id: 1 } as Article,
        ]);
        asWritableSignal(store.pagination).set({
          totalPages: 4,
          selectedPage: 1,
        });
        fixture.detectChanges();

        uiArticleListComponent = fixture.debugElement.queryAll(
          By.directive(UiArticleListComponent)
        )[0]?.componentInstance as UiArticleListComponent;

        uiPaginationComponent = fixture.debugElement.queryAll(
          By.directive(UiPaginationComponent)
        )[0]?.componentInstance as UiPaginationComponent;
      });

      describe('Child component: article list', () => {
        it('should render the articles', () => {
          const uiArticleListComponent = fixture.debugElement.queryAll(
            By.directive(UiArticleListComponent)
          )[0]?.componentInstance as UiArticleListComponent;
          expect(uiArticleListComponent).toBeDefined();
          expect(uiArticleListComponent.articles).toEqual([
            { slug: 'slug 1', id: 1 } as Article,
          ] as Articles);
          expect(screen.queryByText(/loading/i)).toBeNull();
          expect(screen.queryByText(/error1/i)).toBeNull();
        });

        it('should get the article list from the store', () => {
          expect(uiArticleListComponent.articles).toEqual([
            { slug: 'slug 1', id: 1 } as Article,
          ] as Articles);
        });
      });
    });
Enter fullscreen mode Exit fullscreen mode

In the beforeEach() function, we use the asWritableSignal() function to convert the type of the mocked store selector signals to WritableSignal. We set the values of these writable signals so that these simulate how the setRequestStateSuccess() stores the results of a HTTP request in the real store:

  • httpRequestState = HttpRequestStates.FETCHED
  • articles = [{ slug: 'slug 1', id: 1 }]
  • pagination = { totalPages: 4, selectedPage: 1, }

Then we check whether the article list component is rendered or not, and whether it receives the proper articles input form the store.

Auto-mocking Signal Stores

Creating mock stores manually is a time-consuming task, additionally, mock stores need to be updated every time when we change the original store's structure or initial values. It's also challenging to keep the types in sync in the mock and the real stores. Since the mock and the real store are two separate classes, Typescript can't support us.

To overcome these challenges, and automatically generate mock stores like the MockArticleListSignalStore, I created the provideMockSignalStore() function (source code). It generates a mock version of a SignalStore, so it replaces:

  • Signals by WritableSignals
  • Functions by Sinon fakes
  • RxMethods by FakeRxMethods

We can use these mocked SignalStores in unit tests, in Storybook and in Storybook Play tests.

Here is the updated TestBed for the article list component (source code), it uses provideMockSignalStore(). The component's Signal Store and all the child UI components are mocked:

describe('ArticleListComponent_SS - mockComputedSignals: true + mock all child components', () => {
  let component: ArticleListComponent_SS;
  let fixture: ComponentFixture<ArticleListComponent_SS>;
  // we have to use UnwrapProvider<T> to get the real type of a SignalStore
  let store: UnwrapProvider<typeof ArticleListSignalStore>;
  let mockStore: MockSignalStore<typeof store>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        ArticleListComponent_SS,
        MockComponent(UiArticleListComponent),
        MockComponent(UiPaginationComponent),
      ],
      providers: [],
    })
      .overrideComponent(ArticleListComponent_SS, {
        set: {
          providers: [
            // override the component level providers
            MockProvider(ArticlesService), // injected in ArticleListSignalStore
            provideMockSignalStore(ArticleListSignalStore, {
              // if the mockComputedSignals is enabled (default),
              // you must provide an initial value for each computed signal
              initialComputedValues: {
                totalPages: 0,
                pagination: { selectedPage: 0, totalPages: 0 },
              },
            }),
          ],
        },
      })
      .compileComponents();

    fixture = TestBed.createComponent(ArticleListComponent_SS);
    component = fixture.componentInstance;
    // access to a service provided on the component level
    store = fixture.debugElement.injector.get(ArticleListSignalStore);
    mockStore = asMockSignalStore(store);
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode

To access the store's spies and FakeRxMethods in a type safe way, we create the mockStore alias for the store: mockStore = asMockSignalStore(store);, it has a MockSignalStore<ArticleListSignalStore> type.

We can pass the following options to provideMockSignalStore():

  • initialStatePatch: A partial initial state, it overrides the original initial state
  • mockComputedSignals: When true, replaces the computed signals by WritableSignals (default is true). If we set this to false, provideMockSignalStore() keeps the original computed signals
    • initialComputedValues: Initial values for computed signals, we have to provide an initial value for each computed signal in the store, if mockComputedSignals = true
    • mockMethods: When true, replaces methods by Sinon fakes (default is true).
    • mockRxMethods When true, replaces RxMethods by FakeRxMethods (default is true).

We can use the same unit tests we used with the MockArticleListSignalStore, and we can apply patchState() to update the state signals in the store:

    describe('Main UI state: FETCHED', () => {
      let uiPaginationComponent: UiPaginationComponent;
      let uiArticleListComponent: UiArticleListComponent;
      beforeEach(() => {
        // this is the original code, still works:
        // asWritableSignal(store.httpRequestState).set(HttpRequestStates.FETCHED);
        // asWritableSignal(store.articles).set([
        //   { slug: 'slug 1', id: 1 } as Article,
        // ]);

        // simplified version with patchState:
        patchState(store, () => ({
          httpRequestState: HttpRequestStates.FETCHED,
          articles: [{ slug: 'slug 1', id: 1 } as Article],
        }));

        asWritableSignal(store.pagination).set({
          totalPages: 4,
          selectedPage: 1,
        });
        fixture.detectChanges();

        uiArticleListComponent = fixture.debugElement.queryAll(
          By.directive(UiArticleListComponent)
        )[0]?.componentInstance as UiArticleListComponent;

        uiPaginationComponent = fixture.debugElement.queryAll(
          By.directive(UiPaginationComponent)
        )[0]?.componentInstance as UiPaginationComponent;
      });
      describe('Child component: article list', () => {
        it('should get the article list from the store', () => {
          expect(uiArticleListComponent.articles).toEqual([
            { slug: 'slug 1', id: 1 } as Article,
          ] as Articles);
        });
      });

Enter fullscreen mode Exit fullscreen mode

Auto-mocking Signal Stores with Custom Store Features

Custom Store Features provide a mechanism to extend the functionality of SignalStores in a reusable way. They add state signals, computed signals, RxMethods and methods to a Signal Store, so they are completely mockable with the provideMockSignalStore(). Here is an example test for an article list component, that uses a Signal Store together with the withDataService Custom Store Feature: article-list-signal-store-feature.component.auto-mock-everything.spec.ts.

I wrote a detailed article on how the article list component together with the withDataService work: Improve data service connectivity in Signal Stores using the withDataService Custom Store Feature

Auto-mocking Signal Stores in Storybook

You can also use mock Signal Stores generated by the provideMockSignalStore() in Storybook Stories and Storybook Play interaction tests (source code):

// https://github.com/storybookjs/storybook/issues/22352 [Bug]: Angular: Unable to override Component Providers
// We have to create a child class with a new @Component() decorator to override the component level providers

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [UiArticleListComponent, UiPaginationComponent, HttpRequestStateErrorPipe],
  // override the component level providers
  providers: [
    provideMockSignalStore(ArticleListSignalStore, {
      mockComputedSignals: false,
      initialStatePatch: {
        httpRequestState: HttpRequestStates.FETCHED,
        articles: [
          { id: 1, ... },
          { id: 2, ... }
        ],
        articlesCount: 8,
      },
    }),
  ],
  templateUrl: 'article-list-signal-store.component.html',
})
class ArticleListComponent_SS_SB extends ArticleListComponent_SS {}

const meta: Meta<ArticleListComponent_SS_SB> = {
  title: 'ArticleListComponent_SS',
  component: ArticleListComponent_SS_SB,
  decorators: [
    applicationConfig({
      // we can override root level providers here
      providers: [MockProvider(ArticlesService)],
    }),
  ],
  // ...
};

export default meta;
type Story = StoryObj<ArticleListComponent_SS_SB>;

export const Primary: Story = {
  name: 'Play test example',
  args: {},
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // get the component
    const componentEl = canvasElement.querySelector('ng-component');
    // @ts-ignore
    const component = ng.getComponent(componentEl) as ArticleListComponent_SS;

    // get the store as MockSignalStore
    const mockStore = asMockSignalStore(component.store);

    mockStore.setSelectedPage.resetHistory();
    getRxMethodFake(mockStore.loadArticles).resetHistory();

    const nav = within(await canvas.findByRole('navigation'));
    const buttons = await nav.findAllByRole('button');

    // the user clicks on page '2'
    // previous, 0, 1, 2 ...
    await userEvent.click(buttons[3]);
    await waitFor(() => {
      // loadArticles() should be called
      expect(getRxMethodFake(mockStore.loadArticles).callCount).toBe(1);
      // setSelectedPage(2) should be called
      expect(mockStore.setSelectedPage.callCount).toBe(1);
      expect(mockStore.setSelectedPage.lastCall.args).toEqual([2]);
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Storybook currently has a limitation: it can't override component level providers, however it can override module and root level providers by setting the Meta.decorators and providing a new applicationConfig() (GitHub issue). To work around this limitation, we have to create a child class with a new @Component() decorator that contains the mock component-level providers.

Summary

I hope that both my manual and automatic Signal Store mocking techniques will be useful for you. As I demonstrated in this article, these techniques (together with ng-mocks) enable you to set up your tests more efficiently, for instance by mocking component dependencies more smoothly.   

Try out my provideMockSignalStore function, and please let me know how it worked out for you!

👨‍💻About the author

My name is Gergely Szerovay, I work as a frontend development chapter lead. Teaching (and learning) Angular is one of my passions. I consume content related to Angular on a daily basis — articles, podcasts, conference talks, you name it.

I created the Angular Addict Newsletter so that I can send you the best resources I come across each month. Whether you are a seasoned Angular Addict or a beginner, I got you covered.

Next to the newsletter, I also have a publication called Angular Addicts. It is a collection of the resources I find most informative and interesting. Let me know if you would like to be included as a writer.

Let’s learn Angular together! Subscribe here 🔥

Follow me on Substack, Medium, Dev.to, Twitter or LinkedIn to learn more about Angular!

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.