Open markostanimirovic opened 6 months ago
Can take care of it.
Any specific best practices you would like to be included?
Can I join in this?
@rainerhahnekamp sure. We need to clarify whether @markostanimirovic has some specific requirements he would love to put on that page
Hey 👋
Here are the main topics I'd like to be covered:
Optionally:
For more inspiration check how SignalStore Core Concepts and Custom Store Features pages are structured (attention to details, examples, etc.).
Okay, about testing without TestBed: I think the number of stores without DI, will be a minority.
You will very likely have an HttpClient
or a service depending on it almost always. I think that kind a test without a TestBed only applies to testing the store itself (so tests which are part of NgRx), or if for stores managing a UI state.
I would include an example without TestBed at the beginning, but I'd see the main focus on test cases with DI.
When it comes to mocking, we should also have a chapter on mocking the Store itself when it is used inside a component.
rxMethod
should show how to test it with marbles but also without it.
What do you think?
Sounds good to me 👍
Excited for this documentation to land.
Just one addition: I think we should also include some type testing. I find that important for advanced use cases.
excited as well for this documentation. Is there any workaround to spy on patchState and injected services in withMethods ? i have already tried to mock followig @markostanimirovic comment here in angular but it doesn't work:
import * as fromUsersStore from './users.store'; // test jasmine.spyOn(fromUsersStore, 'injectUsersStore').mockReturnValue({ /* fake users store */ });
Hi, you want to create a spy on patchState
itself? I am afraid that will not work. patchState
as standalone is very similar to inject
, which we also cannot spy unless the framework itself provides mocking/spy functionalities.
What I would recommend instead, that you don't spy on the packState but check against the value of the state itself.
So something like:
Store:
const CounterStore = signalStore(
{providedIn: 'root'},
withState({counter: 1})
);
Service which you want to test:
@Injectable({providedIn: 'root'})
export class Incrementer {
store = inject(CounterStore);
increment() {
patchState(this.store, value => ({counter: value.counter + 1}));
}
}
The actual test:
it('should verify that increment increments', () => {
const counterStore = TestBed.inject(CounterStore);
const incrementer = TestBed.inject(Incrementer);
expect(counterStore.counter()).toBe(1);
incrementer.increment();
TestBed.flushEffects();
expect(counterStore.counter()).toBe(2);
})
I didn't try out that code. Might include even some syntax errors, but that's the general approach I would recommend.
Is it what you had in mind?
ok that's clear, what about spy on injected services in withMethods . I want to check that a service is called . in Component Stores we used to use component injection and spy using spyOn:
const spy = spyOn((store as any).injectedService, 'methodInsideInjectedService');
ok that's clear, what about spy on injected services in withMethods . I want to check that a service is called . in Component Stores we used to use component injection and spy using spyOn:
const spy = spyOn((store as any).injectedService, 'methodInsideInjectedService');
In the same way as you do for any other service using TestBed
:
TestBed.configureTestingModule({
providers: [MyStore, { provide: MyService, useValue: { doSomething: jest.fn() } }],
});
const myStore = TestBed.inject(MyStore);
const myService = TestBed.inject(MyService);
myStore.doSomething();
expect(myService.doSomething).toHaveBeenCalled();
In the same way as you do for any other service using
TestBed
:TestBed.configureTestingModule({ providers: [MyStore, { provide: MyService, useValue: { doSomething: jest.fn() } }], }); const myStore = TestBed.inject(MyStore); const myService = TestBed.inject(MyService); myStore.doSomething(); expect(myService.doSomething).toHaveBeenCalled();
Has anyone got this to work? Using UPDATE: I misread this and the solution above still stands for mocking a method on a service used within the Store. My question/issue below is about mocking / spying on an jasmine.createSpy()
I get typing errors due to the Unsubscribable
type required both on result type and on the spy itself.rxMethod
method.
I'm trying to implement a helper method to generate an rxMethod spy but stuck on making it work without manual typecasting etc.:
import type { Signal } from '@angular/core';
import type { Observable, Unsubscribable } from 'rxjs';
// This reproduces the same types from @ngrx/signals (which aren't exported)
type RxMethodInput<Input> = Input | Observable<Input> | Signal<Input>;
type RxMethod<Input> = ((input: RxMethodInput<Input>) => Unsubscribable) & Unsubscribable;
export const buildRxMethodSpy = <Input>(name: string) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const rxMethodFn = (input: RxMethodInput<Input>) => {
return {
unsubscribe: () => {
return;
},
};
};
rxMethodFn.unsubscribe = () => {
return;
};
const spy = jasmine.createSpy<RxMethod<Input>>(name, rxMethodFn);
// Somehow add `.unsubscribe` to the spy in a way that TypeScript understands
return spy;
};
Note: I already tried using jasmine.createSpy()
directly but encountered the typing issues; the above code is likely overkill, assuming there's a solution to typing jasmine.createSpy()
properly.
Update: for now I'm resorting to explicit typecasting:
import type { Signal } from '@angular/core';
import { noop, type Observable, type Unsubscribable } from 'rxjs';
// This reproduces the same types from @ngrx/signals (which aren't exported)
type RxMethodInput<Input> = Input | Observable<Input> | Signal<Input>;
type RxMethod<Input> = ((input: RxMethodInput<Input>) => Unsubscribable) & Unsubscribable;
export const buildRxMethodSpy = <Input>(name: string) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const rxMethodFn = (input: RxMethodInput<Input>) => {
return {
unsubscribe: noop,
};
};
rxMethodFn.unsubscribe = noop;
const spy = jasmine.createSpy<RxMethod<Input>>(name, rxMethodFn) as unknown as jasmine.Spy<
RxMethod<Input>
> &
Unsubscribable;
spy.unsubscribe = noop;
return spy;
};
But would very much like to hear if there's a better way to do this.
@jits it looks like you want to spy on the Store's method itself, right?
The example you are referring to, is about a service which is used in the Store, and you want to spy on a method on that service.
Hi @rainerhahnekamp — ahh yes, good spot, thanks. I missed that aspect. (I'll update my comment to reflect this).
I don't suppose you have a good way to build a mock or spy for an rxMethod
method? (Without resorting to manual type casting).
@jits What do you think about that? https://github.com/ngrx/platform/issues/4256
@rainerhahnekamp — that sounds great! (I'll continue with additional thoughts there)
Most things just work as normal (when you use TestBed), but is there a trick to get computed variables to fire?
withComputed(({request, patient}) => ({
// Computed state from other parts of the state
patientRequestDoctorModel: computed(() =>
request() ? PatientRequestDoctorModel.fromRequest(request()) : PatientRequestDoctorModel.fromPatient(patient())
)
}))
I expected it to work when either request or patient signals changed, but it doesn't fire in the test.
EDIT Nevermind, I'm an idiot, I forgot that computed were lazy. I had just run the other tests that changed request and patient, but I hadn't asserted on patientRequestDoctorModel(), so it didn't run the computed. At least it is a cautionary tale for googlers 😂
Information
The new page should be created for the testing guide:
@ngrx/signals
>SignalStore
>Testing
The guide should explain how SignalStore should be tested with examples.
Documentation page
No response
I would be willing to submit a PR to fix this issue