#StackBounty: #angular #asynchronous #testing #jestjs Angular Jest async test seems to overflow results from one test to another

Bounty: 100

In Angular, using Jest we have 2 tests that test a method on a component class:

  describe('checkEmailStatus', () => {
    it('set VERIFIED page design when email verification succeeds', async () => {
      jest.spyOn(authService, 'checkEmailVerification');
      await expect(component.checkEmailStatus()).resolves.toEqual(undefined);
      expect(authService.checkEmailVerification).toBeCalledTimes(1);
      expect(component.pageDesign.key).toBe('verified');
    });

    it('set ERROR page design when email verification fails', async () => {
      const checkEmail = jest.spyOn(authService, 'checkEmailVerification');
      checkEmail.mockImplementation(() => {
        return Promise.reject(false);
      });
      await expect(component.checkEmailStatus()).resolves.toEqual(undefined);
      expect(authService.checkEmailVerification).toBeCalledTimes(1);
      expect(component.pageDesign.key).toBe('error');
    });
  });

These tests have been running fine for a month. Nothing about this component has changed and neither have we changed Jest version (25.2.7) yet now the 2nd test complains that the method was called 3 times.

If I comment out the first test, the 2nd tests passes.

It seems that the first test is not tearing down correctly – is there something I need to do to force that? (I tried using the done() callback, but it made no difference)

UPDATE

This is the method under test:

  async checkEmailStatus(): Promise<void> {
    this.isLoading = true;
    try {
      await this.authService.checkEmailVerification('');
      this.setPageDesign('verified');
      this.isLoading = false;
    } catch (error) {
      this.setPageDesign('error');
      this.isLoading = false;
    }
  }

This is the stubbed authService:

import {Observable, BehaviorSubject, of} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {mockUsers} from '../../../../mocks/user.mock';

// tslint:disable-next-line: completed-docs
function initStub() {
  const userId$ = new BehaviorSubject<string>(null);

  return {
    userId$,
    checkEmailVerification(): Promise<boolean> {
      return Promise.resolve(true);
    }
  };
}

export const authServiceStub = initStub();

UPDATE 2

This is the complete test file:

import {AuthService} from 'src/app/shared/services/auth.service';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';

import {VerifyEmailComponent} from './verify-email.component';
import {SharedModule} from '../shared/shared.module';
import {getTranslocoModule} from '../transloco-testing.module';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AngularFireModule} from '@angular/fire';
import {environment} from 'src/environments/environment';
import {routerStub} from '../test/helpers/router.stub';
import {authServiceStub} from '../test/helpers/auth.service.stub';

fdescribe('VerifyEmailComponent', () => {
  let component: VerifyEmailComponent;
  let fixture: ComponentFixture<VerifyEmailComponent>;
  let authService: AuthService;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [VerifyEmailComponent],
      imports: [
        SharedModule,
        getTranslocoModule({}),
        BrowserAnimationsModule,
        AngularFireModule.initializeApp(environment.firebase)
      ],
      providers: [routerStub, {provide: AuthService, useValue: authServiceStub}]
    }).compileComponents();
    authService = TestBed.inject(AuthService);
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(VerifyEmailComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  describe('setPageDesign', () => {
    it('should set the correct design for VERIFY', () => {
      component.setPageDesign('verify');
      expect(component.pageDesign.key).toBe('verify');
    });

    it('should set the correct design for VERIFIED', () => {
      component.setPageDesign('verified');
      expect(component.pageDesign.key).toBe('verified');
    });

    it('should set the correct design for ERROR', () => {
      component.setPageDesign('error');
      expect(component.pageDesign.key).toBe('error');
    });

    it('should set the ERROR design for unknown status values', () => {
      component.setPageDesign('');
      expect(component.pageDesign.key).toBe('error');
    });
  });

  describe('checkEmailStatus', () => {
    it('set VERIFIED page design when email verification succeeds', async () => {
      jest.spyOn(authService, 'checkEmailVerification');
      await expect(component.checkEmailStatus()).resolves.toEqual(undefined);
      expect(authService.checkEmailVerification).toBeCalledTimes(1);
      expect(component.pageDesign.key).toBe('verified');
    });

    it('set ERROR page design when email verification fails', async () => {
      const checkEmail = jest.spyOn(authService, 'checkEmailVerification');
      checkEmail.mockImplementation(() => {
        return Promise.reject(false);
      });
      await expect(component.checkEmailStatus()).resolves.toEqual(undefined);
      fixture.detectChanges();

      expect(authService.checkEmailVerification).toBeCalledTimes(1);
      expect(component.pageDesign.key).toBe('error');
    });
  });

  describe('onClickContinue', () => {
    // TODO: implement 2 tests for if/else cases of the button
    return undefined;
  });
});

This is the component code:

import {TranslocoService, TRANSLOCO_SCOPE} from '@ngneat/transloco';
import {Component, OnInit} from '@angular/core';
import {AuthService} from '../shared/services/auth.service';
import {Router} from '@angular/router';

// define static data to be used only by this component
interface PageDesign {
  icon: string;
  key: string;
}
const pageDesigns: PageDesign[] = [
  {
    icon: 'email-verified',
    key: 'verify'
  },
  {
    icon: 'email-verified',
    key: 'verified'
  },
  {
    icon: 'email-expired',
    key: 'error'
  }
];

@Component({
  selector: 'wn-verify-email',
  templateUrl: './verify-email.component.html',
  styleUrls: ['./verify-email.component.scss'],
  providers: [{provide: TRANSLOCO_SCOPE, useValue: 'verifyEmail'}]
})
export class VerifyEmailComponent implements OnInit {
  isLoading: boolean = false;
  pageDesign: PageDesign;

  constructor(
    public translocoService: TranslocoService,
    private authService: AuthService,
    private router: Router
  ) {}

  /**
   * Init
   */
  ngOnInit(): void {
    this.setPageDesign('verify');
    this.checkEmailStatus();
  }

  /**
   * Affects the current email data
   */
  setPageDesign(status: string): any {
    this.pageDesign = pageDesigns.find(
      emailDesign => emailDesign.key === status
    );
    if (!this.pageDesign)
      this.pageDesign = pageDesigns.find(
        emailDesign => emailDesign.key === 'error'
      );
  }

  /**
   * Check whether email address is verified
   */
  async checkEmailStatus(): Promise<void> {
    this.isLoading = true;
    try {
      await this.authService.checkEmailVerification('');
      this.setPageDesign('verified');
      this.isLoading = false;
    } catch (error) {
      this.setPageDesign('error');
      this.isLoading = false;
    }
  }

  /**
   * Click handler for the continue navigation button
   */
  onClickContinue(status: string) {
    if (status === 'verified')
      // TODO: use the continueURL from params and navigate to that
      console.error('continue url needed');
    else this.router.navigate(['/']);
  }
}


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.