Unit testing NGXS states is similar to testing other services. To perform a unit test, we need to set up a store with the states against which we want to make assertions. Then, we dispatch actions, listen to changes, and perform expectations.
We recommend using the selectSnapshot or selectSignal methods instead of select or selectOnce, because it would require calling a done function manually. This actually depends on whether states are updated synchronously or asynchronously. If states are updated synchronously, then selectOnce would always emit updated state synchronously.
💡 selectSnapshot may behave similarly to selectSignal, but it would be more readable because you don't need to call the signal function to get the value.
The above test would fail if the expectation within the subscribe function isn't run once.
Prepping State
Often in your app, you'll need to test what happens when the state is C and you dispatch action X. You can use store.reset(MyNewState) to prepare the state for your next operation.
⚠️ When resetting the state, ensure you provide the registered state name as the key. store.reset affects your entire state. Merge the current state with your new changes to ensure nothing gets lost.
It's also very easy to test asynchronous actions. You can use async/await along with RxJS's firstValueFrom method, which "converts" Observables to Promises. Alternatively, you can use a done callback.
The example below isn't really complex, but it clearly demonstrates how to test asynchronous code using async/await:
import { timer, tap, mergeMap } from'rxjs';it('should wait for completion of the asynchronous action',async () => {classIncrementAsync {static type ='[Counter] Increment async'; }classDecrementAsync {static type ='[Counter] Decrement async'; }// Assume you will make some XHR call to your API or anything elsefunctiongetRandomDelay() {return1000*Math.random(); } @State({ name:'counter', defaults:0 }) @Injectable()classCounterState { @Selector()staticgetCounter(state:number) {return state; } @Action(IncrementAsync)incrementAsync(ctx:StateContext<number>) {constdelay=getRandomDelay();returntimer(delay).pipe(tap(() => {// We're incrementing the state value and setting itctx.setState(state => (state +=1)); }),// After incrementing we want to decrement it again to the zero valuemergeMap(() =>ctx.dispatch(newDecrementAsync())) ); } @Action(DecrementAsync)decrementAsync(ctx:StateContext<number>) {constdelay=getRandomDelay();returntimer(delay).pipe(tap(() => {ctx.setState(state => (state -=1)); }) ); } }TestBed.configureTestingModule({ providers: [provideStore([CounterState])] });conststore:Store=TestBed.inject(Store);awaitfirstValueFrom(store.dispatch(newIncrementAsync()));constcounter=store.selectSnapshot(CounterState.getCounter);expect(counter).toBe(0);});
Collecting Actions
Below is the code used to collect actions passing through the actions stream:
The actions collector snippet above was created by the NGXS team and has been successfully used in production apps for years. Now, let's examine an example of how to set up the collector and how to use it: