Search code examples
javascriptangularrxjsngrx

Testing async Angular validators with NGRX store


I have an async validator for a reactive form which, on blur, dispatches an action. This action triggers an effect, which massages some data that's coming in from a service.

In the validator, I use a selector to listen in on the store, and use rxjs operators to get the data in the form I need. This is working well. However, I am stuck in the testing coverage for this function. Why is my test not covering the whole operation?

Username validator

export const UniqueUsernameValidator = (store) => {
   return (control: any): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
    console.log(store)
    store.dispatch(new CheckUsername(control.value));
      return store.pipe(select(selectUsernameIsUnique)).pipe(
        take(2),
        tap((val) => console.log(val)),
        filter(isUnique => isUnique !== undefined),
        map(isUnique => isUnique ? null : {usernameIsNotUnique: true}),
      )
    }
}

Username validator Spec

describe('UniqueUsernameValidator', () => {
    const storeMock = {
        dispatch() {
            return true
        },
        pipe() {
            return of(true);
        }
      };
    let control = {
        value: 'abc123'
    }
    it('should dispatch an action of CheckUsername', () => {
        UniqueUsernameValidator(storeMock)(control.value)
    })
})

And this is the current coverage: enter image description here

TLDR: Why is it missing this coverage, and is this the right approach to take to mock the store?


Solution

  • You are returning an observable that never gets subscribed to in your test. It is the act of subscribing to the returned observable that run the logic in the

    .pipe(
        take(2),
        tap((val) => console.log(val)),
        filter(isUnique => isUnique !== undefined),
        map(isUnique => isUnique ? null : {usernameIsNotUnique: true}),
     )
    

    You need to test the emitted value from the returned observable.

    let validationResult;
    const sub = UniqueUsernameValidator(storeMock)(control.value).subscribe(result => {
      validationResult = result;
    });
    sub.unsubscribe();
    expect(validationResult.usernameIsNotUnique).toEqual(true);
    

    Seeing you know the observable was created with of you can unwrap it like that but if the observable was async you would need to make an async test.

    I use this helper function to test observables asynchronously.

    import { Observable, Subject } from 'rxjs';
    import { takeUntil } from 'rxjs/operators';
    
    export async function firstEmitted<T>(obs$: Observable<T>): Promise<T> {
        return new Promise<T>(resolve => {
            const finalise = new Subject<void>();
            obs$.pipe(takeUntil(finalise)).subscribe(value => {
                finalise.next();
                resolve(value);
            });
        });
    }
    

    and use it like

    it('should dispatch an action of CheckUsername', async(async () => {
      const validationResult = await firstEmitted(UniqueUsernameValidator(storeMock)(control.value));
      expect(validationResult.usernameIsNotUnique).toEqual(true);
    }));
    

    The

    async(async () =>
    

    confuses some people but the first async is the async function from the angular testing module to state that the test function is an anync function and the second one marks the function as async as we are using the await operator in the function.