Search code examples
angularunit-testingjasmine

Jasmine Unit Test for to check activate route has two parameters and call service function


I have a function in my component to get current route parameters and call a function in a service if two parameters are present.

Component.ts:

listenToRouteParameters(): void {
    const state = this.route.snapshot?.queryParamMap.get('state');
    const code = this.route.snapshot?.queryParamMap.get('code');
    if (state && code) {
      const codeVerifier = this.cookieService.getCookieValue(state);
      if (codeVerifier) {
        this.cookieService.removeCookie();
        this.initiateTokenExchange(code, codeVerifier);
      } else {
        this.refreshTokens();
      }
    } else {
      this.refreshTokens();
    }

}

I wrote a unit test for it as follows.

let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;
  let el: HTMLElement;
  let router: Router;
  let route: ActivatedRoute;
  let cookieService: CookieService;
  const paramsSubject = new BehaviorSubject({
    state: '323232323',
    code: '232323232',
  });

beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ LoginComponent ],
      imports: [
        FormsModule,
        ReactiveFormsModule,
        HttpClientModule,
        RouterTestingModule
      ],
      providers:[
        {
          provide: ActivatedRoute,
          useValue: {
            params: paramsSubject
          },
        },
        { provide: CookieService, useValue: cookieService}
      ]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    router = TestBed.get(Router)
    route = TestBed.get(ActivatedRoute)
  });

  it('should retrieve cookie if current route has state and code params', () => {
    const activatedRoute: ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);

    activatedRoute.queryParams = of({ state: '123' });

    fixture.detectChanges(); 
    //  tick();

    activatedRoute.queryParams.subscribe((value) => {
      expect(cookieService.getCookieValue).toHaveBeenCalled();
    })
  });

This unit test pass regardless of passing parameters or not. Appreciate it if anyone can have a look and tell me how to properly write unit tests for the scenario.

Edit: CookieService.ts

@Injectable({
  providedIn: 'root'
})
export class CookieService {

  constructor() { }

  /**
   * Set cookie
   * @param state State value
   * @param codeVerifier Code verifier value
   */
  setCookie(state: string, codeVerifier: string): void {
    document.cookie = `app.txs.${state}=${codeVerifier};secure;sameSite=strict;`;
  }

  /**
   * Get cookie value
   * @param state 
   * @returns 
   */
  getCookieValue(state: string | null): string | undefined {
    return document.cookie.split('; ').find(row => row.startsWith(`app.txs.${state}=`))?.split('=')[1];
  }

  /**
   * Remove cookie
   */
  removeCookie(): void {
    let cookies = document.cookie.split(";");
    for (let i = 0; i < cookies.length; i++){   
      let spcook =  cookies[i].split("=");
      document.cookie = spcook[0] + "=;expires=Thu, 21 Sep 1979 00:00:01 UTC;";                                
    }
  }
}

Solution

  • You can do something like this, follow the comments with !!:

    // !! change the declaration to this !!
    let cookieService: jasmine.SpyObj<CookieService>;
    beforeEach(async () => {
        // !! add this !!
        // !! the 2nd argument accepts an array of strings that will
        // mock the public methods as spies !!
        cookieService = jasmine.createSpyObj('CookieService', ['getCookieValue', 'removeCookie']);
        await TestBed.configureTestingModule({
          declarations: [ LoginComponent ],
          imports: [
            FormsModule,
            ReactiveFormsModule,
            HttpClientModule,
            RouterTestingModule
          ],
          providers:[
            {
              provide: ActivatedRoute,
              useValue: {
                params: paramsSubject,
                // mock snapshot as well
                snapshot: {
                  queryParamMap: {
                     get: () => {}
                  }
                }
              },
            },
            { provide: CookieService, useValue: cookieService}
          ]
        })
        .compileComponents();
      });
    
      
      it('should retrieve cookie if current route has state and code params', () => {
        // !! you don't need this line, you already have a handle on activatedRoute
        // with route = TestBed.get(ActivatedRoute)
        // const activatedRoute: ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
    
        spyOn(route.snapshot.queryParamMap, 'get').and.callFake(param => {
           // !! mock however you wish
           if (param === 'code') {
              return 1;
           } else if (param === 'state') {
              return 2;
           }
        });
    
        fixture.detectChanges(); 
        
        // !! if listenToRouteParameters is called in the ngOnInit
        // then you won't have to explicitly call it because the first fixture.detectChanges()
        // above calls ngOnInit
        component.listenToRouteParameters();
        
        // !! make your expectation
        expect(cookieService.getCookieValue).toHaveBeenCalled();
      });