@ngrx/signals: mockSignalStore feature
#4427
Replies: 20 comments 15 replies
-
|
(Bear with me as I unravel some thoughts) I'm currently trying to standardise on using ng-mocks. With that I can currently do the following: // In a spec file
// Note: `MockXX` are special ng-mocks helpers.
beforeEach(() => MockBuilder(MyComponent, null).mock(MyStore));
// `buildRxMethodSpy` is my ugly helper method to generate a Jasmine spy with the correct TypeScript signature
// (see: https://github.com/ngrx/platform/issues/4206#issuecomment-1932448978 if you want to get very annoyed).
it("should call the store method on creation", () => {
const methodSpy = MockInstance(
MyStore,
"myRxMethod",
buildRxMethodSpy("myRxMethod")
);
const fixture = MockRender(MyComponent);
const component = fixture.point.componentInstance;
expect(component).toBeTruthy();
expect(methodSpy).toHaveBeenCalled();
});With However, I quite like to let ng-mocks take care of mocking things (in this case, the Store), to keep things consistent and predictable, and then provide spy functions (using |
Beta Was this translation helpful? Give feedback.
-
|
I see. Well, in that case, we might want to discuss a set of testing helpers. |
Beta Was this translation helpful? Give feedback.
-
|
@rainerhahnekamp — I do think having a |
Beta Was this translation helpful? Give feedback.
-
|
Yeah, then it's up to the NgRx guys to decide if that is something we should have or not. |
Beta Was this translation helpful? Give feedback.
-
|
Another suggestion on this topic: #4252 |
Beta Was this translation helpful? Give feedback.
-
|
Oh, that's a coincidence. That's huge. Congrats @gergelyszerovay. We could take this issue as the "issue" and #4252 as the official PR. What I'd like to discuss is the dependency to sinon.js. I think we have three options here:
|
Beta Was this translation helpful? Give feedback.
-
|
Aside: @rainerhahnekamp — you may have intended to use a different link for your point (2) above? |
Beta Was this translation helpful? Give feedback.
-
|
I published an article that compares manual Signal Store mocking and my I chose to use Sinon in my prototype because I believe that many teams are currently using Jasmine and are considering moving to Jest in the long run. For these teams, using test framework-agonistic spies may be the best solution. @rainerhahnekamp We should also consider implementing both options (1) and (3): (1). The basic version of (3). Additionally, we might provide test/mock framework specific tools: these would 'auto-spy' the basic mock store, for example the Sinion version would it replace: The drawback of this approach is that supporting more test/mock frameworks would mean more dependencies. |
Beta Was this translation helpful? Give feedback.
-
|
Great article, @gergelyszerovay! Please give me some days to collect and structure all my thoughts on your implementation. |
Beta Was this translation helpful? Give feedback.
-
|
@rainerhahnekamp any thoughts so far ? Looks like the PR is closed and there will be another direction. Looking at the API of the existing |
Beta Was this translation helpful? Give feedback.
-
|
I see two non-technical reasons that speak against Sinon.
That being said, at the moment I’d like to propose another possibility: The signalStore itself requires an absolute minimum of code to set it up. Maybe we don’t need a mocking feature at all. Let me give you an example: const UserStore = signalStore(
withState({ id: 1, firstname: 'Rudolf', lastname: 'Weber' }),
withMethods((store) => {
return {
async load() {
// ...
},
rename() {
// ...
},
};
}),
withComputed((state) => {
return {
prettyName: computed(() => `${state.firstname()} ${state.lastname()}`),
};
})
);Version without mocking function const MockedStore = signalStore(
withState({ id: 2, firstname: 'Sandra', lastname: 'Smith' }),
withMethods(() => ({ load: jest.fn() })),
withComputed(() => ({ prettyName: signal('Sandra Smith') }))
);
TestBed.configureTestingModule({
providers: [{ provide: UserStore, useClass: MockedStore }],
});
const store = TestBed.inject(UserStore) as unknown as InstanceType<
typeof MockedStore
>;
store.load.mockImplementation(() => {
// mocking behavior
});What do you say? |
Beta Was this translation helpful? Give feedback.
-
|
Hi @rainerhahnekamp, I think your proposal works very well for simple cases (which may be the majority?). I guess it has the same implications as managing any manual mocks — e.g. keeping it in sync with the implementation (especially from a TypeScript typing perspective). I'm not a fan of the manual typecasting as we're then breaking out of TypeScript's protections. Still, this goes a very long way in testing and is a good first port of call. A sticking point I (personally) have right now is mocking As you say, we may not need a full mocking capability (e.g. (Apologies for focusing so much on |
Beta Was this translation helpful? Give feedback.
-
|
I wouldn't have too high expectations for the internal implementation of the mocking libraries. They are very good in terms of typing, but internally, they are a collection of functions created with their specific mocks. We usually use these libraries because writing manual mocks generally requires more code than automatic ones. What if we take 3 or 4 examples and compare them? If the manual turns out to be exactly the same amount of code, I suggest that the documentatios shows how to mock the signalStore properly with the common mocking libraries. About mocking |
Beta Was this translation helpful? Give feedback.
-
To remove Jest/Jasminse/Sinon dependencies we should consider the following API: So users have to specify they preferred mock functions, similarly to ng-mock's custom auto spies works. In the documentation, we can provide sample Jest/Jasminse/Sinon The type of Alternatively, we might provide a configuration file generator, users can specify one of the following: Jest/Jasminse/Sinon, and the generator will generate a signal-store-test-config.ts with the spy framework specific
I think it depends on the application. I've used I expect a higher property number per store if I use Signal Store, because with the Custom Store Features, it's really easy to create Signal Stores with dozens of signals/computeds/methods. For example if I use the withDataService Custom Store Feature, it adds 6 new properties to the store. For example, when we create a store for a component, that can modify a Book object (it can read and write a Book), the store has at least 13 properties. If we add a few selectors an methods, for example for supporting tags for the books, we can easily have 20-30 properties. So this is the store: export const BookStore = signalStore(
withState({ book: Book }),
withDataService({
actionName: 'loadBook',
// ...
}),
withDataService({
actionName: 'saveBook',
// ...
}),
}); The minimal, auto-mocked version of the store: provideMockSignalStore(ArticleListSignalStoreWithFeature, {
spies,
mockComputedSignals: false,
}If we need a pre-loaded book in the mocked store: provideMockSignalStore(ArticleListSignalStoreWithFeature, {
spies,
mockComputedSignals: false,
initialStatePatch: {
book: {
title: "Algorithms + Data Structures = Programs",
author: "Wirth, Niklaus"
},
loadBookRequestState: HttpRequestState.FETCHED
}
}The manually mocked version of the previous store (with a pre-loaded book): const MockedStore = signalStore(
withState({
book: {
title: "Algorithms + Data Structures = Programs",
author: "Wirth, Niklaus"
},
loadBookRequestState: HttpRequestState.FETCHED,
saveBookRequestState: HttpRequestState.INITIAL,
// if we want to mock the computeds
isLoadBookRequestStateInitial: false,
isLoadBookRequestStateFetching: false,
isLoadBookRequestStateFetched: true,
getLoadBookRequestStateError: undefined,
isSaveBookRequestStateInitial: true,
isSaveBookRequestStateFetching: false,
isSaveBookRequestStateFetched: false,
getSaveBookRequestStateError: undefined,
}),
withMethods(() => ({
loadBook: fakeRxMethod(),
saveBook: fakeRxMethod(),
})),
withComputed((state) => ({
// if we want to keep the original logic of computeds:
isLoadBookRequestStateInitial: computed(() => state.loadBookRequestState() === HttpRequestStates.INITIAL),
isLoadBookRequestStateFetching: computed(() => state.loadBookRequestState() === HttpRequestStates.FETCHING),
isLoadBookRequestStateFetched: computed(() => state.loadBookRequestState() === HttpRequestStates.FETCHED),
getLoadBookRequestStateError: computed(() => state.loadBookRequestState() === HttpRequestStates.FETCHED),
isSaveBookRequestStateInitial: computed(() => state.saveBookRequestState() === HttpRequestStates.INITIAL),
isSaveBookRequestStateFetching: computed(() => state.saveBookRequestState() === HttpRequestStates.FETCHING),
isSaveBookRequestStateFetched: computed(() => state.saveBookRequestState() === HttpRequestStates.FETCHED),
getSaveBookRequestStateError: computed(() => state.saveBookRequestState() === HttpRequestStates.FETCHED),
}))
);So it's possible to manually mock stores like this, but I think it's a time-consuming task. Automatic mocking can also reduce the required amount of coding, when we add new features to the store later.
This also depends on the use case. I like Storybook, and prefer to make Stories:
So If I want to mock a Signal Store of a smart or dumb component for use in Storybook, I need to provide meaningful values for all the state signals and computeds, otherwise the component will not render correctly in Storybook. I like and use ng-mocks, but I found that
To mock these, I've created a fakeRxMethod function, we can also use this inside a |
Beta Was this translation helpful? Give feedback.
-
|
I'm sorry for not getting back to you sooner. For some reason, I've missed your answer. I was also thinking about providing something like We can provide the spies property for different testing frameworks without requiring new dependencies. The testing library, for example provides jest-specific functions but doesn't list jest as a dependency: https://github.com/testing-library/angular-testing-library/blob/main/projects/testing-library/package.json. @markostanimirovic: The question is how should we proceed? I think we kind of agree that we want to have that mocking feature. |
Beta Was this translation helpful? Give feedback.
-
|
Hey 👋 I'm going to convert this issue into a discussion for now. Feel free to create a standalone library/prototype with To consider introducing a testing plugin as part of the core package, it has to be independent of testing frameworks. |
Beta Was this translation helpful? Give feedback.
-
|
Hey, I'm really glad that this is going forward :) I'm going to update my initial implementation (https://github.com/gergelyszerovay/store-extensions/tree/main/projects/mock-signal-store/src), and I'm going to:
|
Beta Was this translation helpful? Give feedback.
-
|
I pushed an updated prototype and a demo app with a readme file containing some explanation. This version of
In the mock version of a
Source code for
This |
Beta Was this translation helpful? Give feedback.
-
|
Thank you for your nice work, @gergelyszerovay ! Really sorry to hear that the development will not be continued. SignalStore will be more and more common, and it really lacks good testing support. @markostanimirovic Is there really no way to include that into the Ngrx core package? I think a lot of people would benefit from that. |
Beta Was this translation helpful? Give feedback.
-
|
Hello everyone, It is typed, inherits all the initial values from the original signal store, so you don't have to provide them. Of course you can override them just like the methods. All withMethods are mocked functions either mocked by jasmine or jest, depends on the environment. All inject(YourService) is also mocked, you don't have to call the createSignalStoreMock method in an injection context, but you can still provide your injected factory function for withState if needed. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Which @ngrx/* package(s) are relevant/related to the feature request?
signals
Information
It would be great, if we can provide a similar
provideMockStorefeature whenever we want to mock the SignalStore. It would also containjest.fnorjasmine.createSpyfor the methods and computeds:And then inside a test
Describe any alternatives/workarounds you're currently using
At the moment, I'd have to do that manually.
I would be willing to submit a PR to fix this issue
Beta Was this translation helpful? Give feedback.
All reactions