I have the following service in Angular16:
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly userRoles$: Observable<string[]> =
this.fetchUserRoles().pipe(shareReplay(1)); //<-- get user info only once
constructor(private readonly externalService: ExternalService) {}
fetchUserRoles(): Observable<string[]> {
return new Observable<string[]>((observer) => {
this.externalService.fetchUserInfo((error, claims) => {
let userRoles: string[] = [];
if (error) {
console.error(
'An error has occurred',
error
);
throw new Error(error.message);
}
const authorizations = claims.user_authorization.find((auth) => auth.resource === 'MY_APP')?
.permissions.map((permission) => permission.name) ?? [];
observer.next(userRoles);
});
});
}
isUserAuthorized$(): Observable<boolean> {
return this.userRoles$.pipe(map((roles) => roles.length > 0));
}
}
I want to test the isUserAuthorized$()
method and I created the following spec class:
describe('AuthService', () => {
let externalService: jasmine.SpyObj<ExternalService>;
let authService: AuthService;
beforeEach(() => {
const externalServiceSpy = jasmine.createSpyObj('ExternalService', [
'fetchUserInfo'
]);
TestBed.configureTestingModule({
providers: [
{ provide: ExternalService, useValue: externalService },
AuthService
]
});
externalService = TestBed.inject(
ExternalService
) as jasmine.SpyObj<ExternalService>;
authService = TestBed.inject(AuthService);
});
it('Should mark user as authorized because he has ADMIN role', fakeAsync(() => {
externalService.fetchUserInfo.and.stub();
spyOn(authService, 'fetchUserRoles').and.returnValue(of(['ADMIN']));
let result;
authService.isUserAuthorized$().subscribe((value) => (result = value));
tick();
expect(result).toBeTrue();
}));
});
Unfortunately I keep on getting the error: Expected undefined to be true.
My guess is that I mock the fetchUserRoles
method too late (after the service is instantiated and the real fetchUserRoles
method is called). I also tried the async / await test approach, but I get a timeout error. Can you please help?
One problem I notice is that you need to set the spy object for useValue
, but you are setting the actual class.
The main problem is you are setting fetchUserRoles
after the class is initialized, so you will not get proper values for this.userRoles$
since during execution, it did not have the spy value. To fix this, you can reinitialize the property with the proper value.
describe('AuthService', () => {
let externalService: jasmine.SpyObj<ExternalService>;
let authService: AuthService;
beforeEach(() => {
const externalServiceSpy = jasmine.createSpyObj('ExternalService', [
'fetchUserInfo'
]);
TestBed.configureTestingModule({
providers: [
{ provide: ExternalService, useValue: externalServiceSpy },
AuthService
]
});
externalService = TestBed.inject(
ExternalService
) as jasmine.SpyObj<ExternalService>;
authService = TestBed.inject(AuthService);
});
it('Should mark user as authorized because he has ADMIN role', fakeAsync(() => {
externalService.fetchUserInfo.and.stub();
spyOn(authService, 'fetchUserRoles').and.returnValue(of(['ADMIN']));
(authService as any).userRoles$ = authService.fetchUserRoles().pipe(shareReplay(1)); // <-- override the field here
let result;
authService.isUserAuthorized$().subscribe((value) => (result = value));
flush();
expect(result).toBeTrue();
}));
});