Unit testing is easy with NGXS. To perform a unit test we just dispatch the events, listen to the changes and perform our expectation. A basic test looks like this:
We recommend using selectSnapshot method instead of selectOnce or select. Jasmine and Jest might not run expectations inside the subscribe block. Given the following example:
it('should select zoo', () => { store.selectOnce(state =>state.zoo).subscribe(zoo => {// Note: this expectation will not be run!expect(zoo).toBeTruthy(); });constzoo=store.selectSnapshot(state =>state.zoo);expect(zoo).toBeTruthy();});
Prepping State
Often times in your app you want to test what happens when the state is C and you dispatch action X. You can use the store.reset(MyNewState) to prepare the state for your next operation.
Note: You need to provide the registered state name as key if you reset the state. store.reset will reflect to your whole state! Merge the current with your new changes to be sure nothing gets lost.
It's also very easy to test asynchronous actions using Jasmine or Jest. The greatest features of these testing frameworks is a support of async/await. No one prevents you of using async/await + RxJS firstValueFrom method that "converts" Observable to Promise. As an alternative you could have a done callback, Jasmine or Jest will wait until the done callback is called before finishing the test.
The below example is not really complex, but it clearly shows 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 { @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({ imports: [NgxsModule.forRoot([CounterState])] });conststore:Store=TestBed.inject(Store);awaitfirstValueFrom(store.dispatch(newIncrementAsync()));constcounter=store.selectSnapshot(CounterState);expect(counter).toBe(0);});