Search code examples
angularunit-testingjasminekarma-jasminespy

Jasmine spyOn service dependency


I am having difficulty understanding jasmine spies for services with dependencies injected in constructor

I have an Account service with Constructor

constructor(private readonly api: ApiService,private readonly authorityService: AuthorityService) {}

In the Account service I have a method that is using the authorityService, (set in the constructor), to check for a permission before returning a value

  public async getAccount(): Promise<Account | null> {
    if (this.authorityService.hasAuthority(Authority.PERM_ACCOUNT_READ)) {
      this.account = await this.api.get<Account>('/admin/rest/account');
      return this.account;
    }
    return null;
  }

What I am trying to do is provide unit tests for the getAccount method. What I want is to ensure when authorityService.hasAuthority(Authority.PERM_ACCOUNT_READ) returns true or false

In my test I have set up the following in my .spec.ts file

stub the authService

class  authorityServiceStub{
  hasAuthority(str: string): boolean {
    return false;
  }
}

then in my describe block I have the following

fdescribe('AccountService', () => {
  let service: AccountService;
  let httpTestingController:HttpTestingController
  let authorityServiceDependency:AuthorityService;

  /**
   * Sets up the testing module before each test case.
   */
   beforeEach(waitForAsync(() => {

    TestBed.configureTestingModule({
      imports: [],
      providers: [provideHttpClient(),
        provideHttpClientTesting(),
        AccountService,{
          provide: AuthorityService,
          useClass: authorityServiceStub
        }
        ],
    })

    service = TestBed.inject(AccountService);
    authorityServiceDependency = TestBed.inject(AuthorityService);
    httpTestingController = TestBed.inject(HttpTestingController);
  }));

in my first test where I want to ensure if the permission is false, my Account object from getAccount() in null

  it('should return null if not permissions and not forced', async() => {
    spyOn(authorityServiceDependency, 'hasAuthority').withArgs(Authority.PERM_ACCOUNT_READ).and.returnValue(false);
    const account = await service.getAccount();
    expect(account).toBeNull();
    expect(authorityServiceDependency.hasAuthority).toHaveBeenCalledWith(Authority.PERM_ACCOUNT_READ);
    const req = httpTestingController.match((request) =>
      request.url.includes("/admin/rest/account")
    );
  });

This test passes

but in the other case where I want to test when the permission is set and validate the account is not null I have the following

  it('should return the account if the permissions are set', async() => {
    const accountData = {firstName: 'Test',lastName:"tester"} as Account;
    spyOn(authorityServiceDependency, 'hasAuthority').withArgs(Authority.PERM_ACCOUNT_READ).and.returnValue(true);
    spyOn(service, 'getAccount').and.returnValue(Promise.resolve(accountData));
    service.getAccount().then((account) => {
       expect(account).not.toBeNull();
       expect(account?.firstName).toEqual("Test");
       expect(account?.lastName).toEqual("tester");
       expect(service.getAccount).toHaveBeenCalled();

     });
    expect(authorityServiceDependency.hasAuthority).toHaveBeenCalledWith(Authority.PERM_ACCOUNT_READ);
    const req = httpTestingController.match((request) =>
      request.url.includes("/admin/rest/account")
    );
  });

the expect expect(authorityServiceDependency.hasAuthority).toHaveBeenCalledWith(Authority.PERM_ACCOUNT_READ);

is failing with message

Expected spy hasAuthority to have been called with: [ 'PERM_ACCOUNT_READ' ] but it was never called. at at UserContext. (src/app/core/services/account/account.service.spec.ts:86:53) at Generator.next () at asyncGeneratorStep (node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:3:1) at _next (node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:17:1)

I know its something I'm doing incorrectly but for the life of me I cannot get this to work as I want. Not clear is setting up the mock class in my test is correct approach.. seems clunky but I read this approach somewhere on the internet and it seems to make sense but I'm kinda out of my element here and would appreciate any insight / guidance


Solution

    1. You should not set the return value of the method you want to test this is because the method never get's executed, instead spy on it and use .and.callThrough() so that the method is both spied and executed.

    2. Now you can get the apiService and spy on the get method and set the promise return value here spyOn(apiServiceDependency, 'get').and.returnValue( Promise.resolve(accountData) );

    3. Instead of placing test cases outside and inside a then block, just await the function and capture the response so that all tests are executed at once without and synchronicity issues.


    it('should return the account if the permissions are set', async () => {
      const accountData = { firstName: 'Test', lastName: 'tester' } as any;
      spyOn(authorityServiceDependency, 'hasAuthority')
        // .withArgs(Authority.PERM_ACCOUNT_READ)
        .and.returnValue(true);
      spyOn(apiServiceDependency, 'get').and.returnValue(
        Promise.resolve(accountData)
      );
      spyOn(service, 'getAccount').and.callThrough();
      const account = await service.getAccount();
      expect(account).not.toBeNull();
      expect(account?.firstName).toEqual('Test');
      expect(account?.lastName).toEqual('tester');
      expect(service.getAccount).toHaveBeenCalled();
      expect(authorityServiceDependency.hasAuthority).toHaveBeenCalledWith(
        Authority.PERM_ACCOUNT_READ
      );
      const req = httpTestingController.match((request) =>
        request.url.includes('/admin/rest/account')
      );
    });
    

    Full Code:

    import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing';
    import {
      AccountService,
      ApiService,
      AppComponent,
      Authority,
      AuthorityService,
    } from './app.component';
    import { AppModule } from './app.module';
    import { provideHttpClient } from '@angular/common/http';
    import {
      HttpTestingController,
      provideHttpClientTesting,
    } from '@angular/common/http/testing';
    
    class authorityServiceStub {
      hasAuthority(str: string): boolean {
        return false;
      }
    }
    
    describe('AccountService', () => {
      let service: AccountService;
      let httpTestingController: HttpTestingController;
      let authorityServiceDependency: AuthorityService;
      let apiServiceDependency: ApiService;
    
      /**
       * Sets up the testing module before each test case.
       */
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          imports: [],
          providers: [
            provideHttpClient(),
            provideHttpClientTesting(),
            AccountService,
            {
              provide: AuthorityService,
              useClass: authorityServiceStub,
            },
          ],
        });
    
        service = TestBed.inject(AccountService);
        authorityServiceDependency = TestBed.inject(AuthorityService);
        apiServiceDependency = TestBed.inject(ApiService);
        httpTestingController = TestBed.inject(HttpTestingController);
      }));
    
      it('should return null if not permissions and not forced', async () => {
        spyOn(authorityServiceDependency, 'hasAuthority')
          .withArgs(Authority.PERM_ACCOUNT_READ)
          .and.returnValue(false);
        const account = await service.getAccount();
        expect(account).toBeNull();
        expect(authorityServiceDependency.hasAuthority).toHaveBeenCalledWith(
          Authority.PERM_ACCOUNT_READ
        );
        const req = httpTestingController.match((request) =>
          request.url.includes('/admin/rest/account')
        );
      });
    
      it('should return the account if the permissions are set', async () => {
        const accountData = { firstName: 'Test', lastName: 'tester' } as any;
        spyOn(authorityServiceDependency, 'hasAuthority')
          // .withArgs(Authority.PERM_ACCOUNT_READ)
          .and.returnValue(true);
        spyOn(apiServiceDependency, 'get').and.returnValue(
          Promise.resolve(accountData)
        );
        spyOn(service, 'getAccount').and.callThrough();
        const account = await service.getAccount();
        expect(account).not.toBeNull();
        expect(account?.firstName).toEqual('Test');
        expect(account?.lastName).toEqual('tester');
        expect(service.getAccount).toHaveBeenCalled();
        expect(authorityServiceDependency.hasAuthority).toHaveBeenCalledWith(
          Authority.PERM_ACCOUNT_READ
        );
        const req = httpTestingController.match((request) =>
          request.url.includes('/admin/rest/account')
        );
      });
    });