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
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.
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) );
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')
);
});
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')
);
});
});