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

This article demonstrates two distinct strategies for mocking Signal Stores in your tests:

  • constructing mock Signal Stores manually, and
  • leveraging the provideMockSignalStore helper (which produces a mock version of a Signal Store) to cut down on boilerplate

Both approaches follow the same core idea: they turn signals into writable signals, swap out functions with Sinon fakes, and replace RxMethods with fake versions inside the Signal Store. When you combine these techniques with the ng-mocks library, you can streamline your testing setup by mocking component dependencies — services, stores, and child UI components alike — without much ceremony. The same mock store strategy works both for standard unit tests and for Storybook Play interaction tests.

I'll walk you through unit testing a component backed by a Signal Store using mock stores, and then show you how to write a Storybook story with an interaction test for the same component, again using a mock store underneath.

Source code and demo app

The complete source for the mock Signal Store provider lives here: provideMockSignalStore

Demo app source:

Prerequisites

Before diving in, you'll want a solid working knowledge of Signals and Signal Store:

Angular Signals is the new reactivity model that landed in Angular 16. With Signals, you can track state changes in your application and have the template update efficiently in response. If you're just getting started with Signals, these resources are great places to begin:

The NgRx team, along with Marko Stanimirović, introduced SignalStore, a signal-based state management library. If Signal Stores are new to you, Manfred Steyer's four-part series on the topic is worth reading:

You should also be comfortable with how RxMethods function before proceeding.

Article list component

Our demo app contains the ArticleListComponent_SS component. This is a smart component that owns a component-level store called 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 receives two signal inputs, fed from the router via withComponentInputBinding():

  • selectedPage
  • pageSize

Based on the state held in the store, the component renders a pair of dumb/UI components:

  • an article list component, and
  • a pagination component

The component triggers store updates under these circumstances:

  • the selectedPage or pageSize values change in the URL, or
  • the user clicks to a different page using the pagination component

Signal Store for the Article list component

The store's state is defined as follows (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

Here's the Signal Store implementation 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 exposes these state signals: selectedPage: number, pageSize: number, httpRequestState: HttpRequestState, articles: Articles, articlesCount: number.

It also provides:

  • computed selectors: totalPages and withComputed
  • state-updating methods: setSelectedPage, setPageSize, setRequestStateLoading, setRequestStateSuccess, setRequestStateError, and
  • an rxMethod named loadArticles that pulls the article list from ArticlesService

Crafting Mocks for Signal Stores

Suppose a smart component with substantial business logic—such as coordination across app-level, feature-level, and component-level stores—needs to be exercised through unit tests. In that scenario, having the option to replace the component's dependencies with mocks—including services, stores, and child components—is often advantageous.

For mocking plain services and child components, the MockComponent() and MockProvider() functions from the ng-mocks library are my usual tools.

However, MockProvider() falls short when dealing with NgRx ComponentStores and SignalStores. It lacks support for the update() and effect() methods of ComponentStore and fails to handle RxMethods. This is why a tailored mock for ArticleListSignalStore is necessary. To achieve this, we make specific substitutions:

  • Signals become WritableSignals
  • Functions are transformed into Sinon fakes
  • RxMethods are superseded by FakeRxMethods

This strategy grants us direct control over the selector signals' values during a unit test and lets us assert that functions and RxMethods have been invoked, along with inspecting their arguments.

The FakeRxMethods come from the newFakeRxMethod() factory (source code). A FakeRxMethod acts as a function that can accept a static value, a signal, or an observable. Each instance carries a FAKE_RX_METHOD property and holds a Sinon fake. This embedded Sinon fake records invocation details under these circumstances: when called

  • with a static value
  • with a signal input whose value changes
  • with an observable input that emits

Here is an example TestBed for the article list component (source code). Observe that both the component's Signal Store and all child UI components have been 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

This approach involves hand-crafting the MockArticleListSignalStore, which means it must be updated manually whenever the structure of ArticleListSignalStore changes.

To capture the type of the Signal Store, we employ UnwrapProvider<typeof ArticleListSignalStore>, since calling signalStore() yields a provider rather than the store itself.

We leverage the overrideComponent() function to swap out the component-level providers of the article list component. The store instance is retrieved from the component via store = fixture.debugElement.injector.get(ArticleListSignalStore);. What we get back is the mocked rendition of the original store.

Now we can proceed to write unit tests.

Within the constructor() of the article list, there's an effect() which reads the selectedPage() and pageSize() signals from the router, pushes these values into the store, and finally invokes store.loadArticles() to fetch data. Consequently, once the component is created in the test environment and change detection is triggered via detectChanges(), both the effect and loadArticles() should fire:

  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 offers access to the Sinon fake containing the call details for the FakeRxMethod.

Another verification can confirm that the effect triggers store.loadArticles() when the selectedPage() input shifts:

  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 modify the component's input, as this technique also supports signal inputs. Then we call detectChanges() to initiate change detection, which re-runs the effects. Finally, we expect store.loadArticles() to have been invoked.

The next test presents a scenario where the article list is already loaded, and we verify that it renders correctly and receives its 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

Inside the beforeEach() function, the asWritableSignal() function helps convert the type of the mocked store selector signals to WritableSignal. We configure these writable signals to mimic the state that setRequestStateSuccess() would establish after a successful HTTP request in the real store:

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

Following that, we confirm whether the article list component is shown and whether it obtains the correct articles input from the store.

Signal Store Auto-mocking

Hand-writing mock stores is laborious; also, these mocks require maintenance with every modification to the original store's structure or initial state. Keeping the types aligned between mocks and actual stores is equally tricky, especially since TypeScript offers no assistance when the mock and real store are separate classes.

To address these complexities and streamline the creation of mocks akin to MockArticleListSignalStore, I developed the provideMockSignalStore() function (source code). This function generates a mocked version of a SignalStore by performing these replacements:

  • Signals are swapped for WritableSignals
  • Functions are replaced by Sinon fakes
  • RxMethods are substituted with FakeRxMethods

These auto-generated mock SignalStores can serve in unit tests, Storybook stories, and Storybook Play tests.

Shown below is the revised TestBed for the article list component (source code), now utilizing provideMockSignalStore(). The component's Signal Store and all child UI components are again 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 manner, we establish a mockStore alias for the store: mockStore = asMockSignalStore(store);, which carries the MockSignalStore<ArticleListSignalStore> type.

These options are available for configuration in provideMockSignalStore():

  • initialStatePatch: A partial initial state that overrides the original initial state
  • mockComputedSignals: If true, converts computed signals into WritableSignals (the default is true). Setting this to false preserves the original computed signals
    • initialComputedValues: Starting values for computed signals, mandatory for each computed signal in the store when mockComputedSignals = true
    • mockMethods: If true, substitutes methods with Sinon fakes (the default is true).
    • mockRxMethods: If true, replaces RxMethods with FakeRxMethods (the default is true).

The unit tests written for the MockArticleListSignalStore are fully compatible with this approach, and we can use patchState() to modify state signals within 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

Leveraging Custom Store Features for Automatic Mocking

Custom Store Features offer a reusable way to extend the capabilities of SignalStores. They introduce state signals, computed signals, RxMethods, and methods into a Signal Store, all of which can be automatically mocked using the provideMockSignalStore() function. Consider an article list component test that combines a Signal Store with the withDataService Custom Store Feature, as shown in this example: article-list-signal-store-feature.component.auto-mock-everything.spec.ts.

If you'd like to dive deeper into how the article list component interacts with the withDataService feature, I've written a dedicated piece: Improve data service connectivity in Signal Stores using the withDataService Custom Store Feature.

Applying Automatic Mocking in Storybook

The mock Signal Stores produced by provideMockSignalStore() can also be incorporated into Storybook Stories and Play interaction tests—you can see an implementation here:

// 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

A current limitation in Storybook prevents overriding component-level providers, though module and root providers can still be overridden by configuring Meta.decorators with a fresh applicationConfig() (tracked in this GitHub issue). To work past this constraint, the approach involves creating a derived class paired with a new @Component() decorator that supplies the mock providers at the component level.

Wrapping Up

I trust that both the manual and automated approaches to mocking Signal Stores presented here will prove valuable in your projects. As highlighted throughout this guide, these strategies—particularly when paired with ng-mocks—allow for a more streamlined test setup, like simplifying how you mock component dependencies.

Feel free to experiment with the provideMockSignalStore utility, and don't hesitate to share your feedback on how it performs for you!

📝 Meet the Author

I'm Gergely Szerovay, a frontend development chapter lead by day. Angular is something I'm deeply passionate about—both learning it and sharing that knowledge. I'm constantly engaging with Angular content, from articles and podcasts to conference talks.

That passion led me to create the Angular Addict Newsletter, a monthly digest of the most valuable resources I stumble upon, tailored for both seasoned developers and those just starting their Angular journey.

Alongside the newsletter, I run a publication called Angular Addicts, a curated collection of the most insightful content I encounter. If you're interested in contributing as a writer, just let me know.

Let's explore Angular together! Subscribe here 🔥

You can connect with me on Substack, Medium, Dev.to, Twitter, or LinkedIn for more Angular insights!