How to mock NgRx Signal Stores for unit tests and Storybook play interaction tests (both manually and automatically)
This article is also available on dev.to with better source code syntax highlighting.
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 RxMethod
s 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:
“Signals in Angular – How to Write More Reactive Code” by Deborah Kurata
“Angular & signals. Everything you need to know” by Robin Goetz
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:
Smarter, Not Harder: Simplifying your Application With NGRX Signal Store and Custom Features
NGRX Signal Store Deep Dive: Flexible and Type-Safe Custom Extensions
It's also important to understand how RxMethod
s 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);
});
}
}
<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 }}
}
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
orpageSize
values change in the URL, orthe 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
}
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(...),
})),
);
The store has the following state signals: selectedPage: number
, pageSize: number
, httpRequestState: HttpRequestState
, articles: Articles
, articlesCount: number
.
It also has:
computed selectors:
totalPages
andwithComputed
methods for updating its state:
setSelectedPage
,setPageSize
,setRequestStateLoading
,setRequestStateSuccess
,setRequestStateError
, andan
rxMethod
for fetching the article list from theArticlesService
: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 ComponentStore
s and SignalStore
s, as it doesn't support ComponentStore
's update()
and effect()
function, nor does it support RxMethod
s. So we create a custom mock version of ArticleListSignalStore
. In order to do that, we replace:
Signal
s byWritableSignal
sFunction
s by Sinon fakesRxMethod
s byFakeRxMethod
s
This way we can explicitly set the selector signals' values in the unit test, and also check whether the functions and RxMethod
s were called and with what parameters.
FakeRxMethod
s 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();
});
});
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);
});
});
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);
});
});
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);
});
});
});
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:
Signal
s byWritableSignal
sFunction
s by Sinon fakesRxMethod
s byFakeRxMethod
s
We can use these mocked SignalStore
s 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();
});
});
To access the store's spies and FakeRxMethod
s 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 statemockComputedSignals
: Whentrue
, replaces the computed signals byWritableSignal
s (default istrue
). If we set this tofalse
,provideMockSignalStore()
keeps the original computed signalsinitialComputedValues
: Initial values for computed signals, we have to provide an initial value for each computed signal in the store, ifmockComputedSignals = true
mockMethods
: Whentrue
, replaces methods by Sinon fakes (default istrue
).mockRxMethods
Whentrue
, replacesRxMethod
s byFakeRxMethod
s (default istrue
).
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);
});
});
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, RxMethod
s 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]);
});
},
};
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!
withComputed(({ selectedPage, totalPages }) => ({
pagination: computed(() => ({ selectedPage: selectedPage(), totalPages: totalPages() })),
})),
Is this possible? If not used//@ ts ignore
WithComputed cannot recognize all attributes in the store
I have read some articles (Angular official docs maybe?) saying that "making reactive calls inside effects is not recommended".
I personally try to avoid them, but in some edge cases I've been forced to circumvent this "rule".
I usually have this problem when i need to use data from store1 to trigger some reaction in store2.
and i usually use the observable mode `toObservable(mySignal)` to circumvent the error.
As you showed, there is more than one way to circumvent this, but I'd love to hear your thoughts about that design pattern.
Are there cases where having reactive calls inside effects are recommended?
would you see this rule as important for the overall app design?
Thanks for the great article.