I am new to unit testing and just decided to add test coverage for my existing app. I am struggling in writing the unit test cases for the given service . I have created a AWS service file .
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Router } from '@angular/router';
import { Auth } from 'aws-amplify';
import { IUser } from '../../models/userModel';
@Injectable({
providedIn: 'root'
})
export class AwsAmplifyAuthService {
private authenticationSubject: BehaviorSubject<any>;
constructor(private router: Router) {
this.authenticationSubject = new BehaviorSubject<boolean>(false);
}
// SignUp
public signUp(user: IUser): Promise<any> {
return Auth.signUp({
username: user.name,
password: user.password,
attributes: {
email: user.email,
name:user.name
}
});
}
// Confirm Code
public confirmSignUp(user: IUser): Promise<any> {
return Auth.confirmSignUp(user.name, user.code);
}
// SignIn
public signIn(user: IUser): Promise<any> {
return Auth.signIn(user.name, user.password)
.then((r) => {
console.log('signIn response', r);
this.authenticationSubject.next(true);
if (user) {
if (r.challengeName === 'NEW_PASSWORD_REQUIRED') {
this.router.navigateByUrl('/forgotpass');
} else {
this.router.navigateByUrl('/home');
}
}
});
}
and created a interface model
export interface IUser {
email: string;
password: string;
code: string;
name: string;
}
I have tired the mocking the service in the .spec.
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [
{
provide: Auth,
useValue: { currentUserInfo: () => Promise.resolve('hello') },
},
]
})
.compileComponents();
service = TestBed.inject(AwsAmplifyAuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('#signIn should return expected data', async (done: any) => {
await service.signIn(expectedData).then(data => {
expect(data).toEqual(expectedData);
done();
});
});
I am not able to pass the test case. Any guidance on the same would be appreciated.
CodePudding user response:
Unfortunately, this is one of the shortcomings of Angular unit testing where it is extremely difficult to mock an import of:
import { Auth } from 'aws-amplify';
How you have mocked it with the providers
approach would work if Auth
was injected in the constructor of the component.
There were ways to mock in the older versions of TypeScript but due to updates, it is difficult to mock imported functions/classes.
What you can do is create a wrapper class for your AWSAmplify like so:
import { Auth } from 'aws-amplify';
class AWSAmplifyWrapper {
static getAuth() {
return Auth;
}
}
In your service, you would have to do:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Router } from '@angular/router';
import { AWSAmplifyWrapper } from './aws-amplify-wrapper';
import { IUser } from '../../models/userModel';
@Injectable({
providedIn: 'root'
})
export class AwsAmplifyAuthService {
private authenticationSubject: BehaviorSubject<any>;
constructor(private router: Router) {
this.authenticationSubject = new BehaviorSubject<boolean>(false);
}
// SignUp
public signUp(user: IUser): Promise<any> {
return AWSAmplifyWrapper.getAuth().signUp({
username: user.name,
password: user.password,
attributes: {
email: user.email,
name:user.name
}
});
}
// Confirm Code
public confirmSignUp(user: IUser): Promise<any> {
return AWSAmplifyWrapper.getAuth().confirmSignUp(user.name, user.code);
}
// SignIn
public signIn(user: IUser): Promise<any> {
return AwsAmplify.getAuth().signIn(user.name, user.password)
.then((r) => {
console.log('signIn response', r);
this.authenticationSubject.next(true);
if (user) {
if (r.challengeName === 'NEW_PASSWORD_REQUIRED') {
this.router.navigateByUrl('/forgotpass');
} else {
this.router.navigateByUrl('/home');
}
}
});
}
And in your unit test:
import { AWSAplifyWrapper } from './aws-amplify-wrapper';
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
})
.compileComponents();
service = TestBed.inject(AwsAmplifyAuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('#signIn should return expected data', async () => {
spyOn(AWSAmplifyWrapper, 'getAuth').and.returnValue({
signIn: () => Promise.resolve({/* mock how you wish here */});
// likewise you can mock signUp and confirmSignUp the same
});
const data = await service.signIn(expectedData);
// your assertions on data
// I would assert whether router.navigateByUrl was called or not
expect(data).toEqual(expectedData);
});
See https://github.com/jasmine/jasmine/issues/1414 and https://stackoverflow.com/a/62935131/7365461 for more details.
CodePudding user response:
Always inject all dependencies instead of accessing them directly from your code. With this approach, you can always mock them where it's needed.
For example, you can create a root token to inject Auth
:
export const AWS_AMPLIFY_AUTH = new InjectionToken('AWS_AMPLIFY_AUTH', {
factory: () => Auth,
providedIn: 'root',
});
then you can inject it in your service like @Inject(AWS_AMPLIFY_AUTH) private auth: typeof Auth
, and use this.auth
to call methods:
@Injectable({
providedIn: 'root',
})
class AwsAmplifyAuthService {
private authenticationSubject: BehaviorSubject<any>;
constructor(
private router: Router,
// INJECTION
@Inject(AWS_AMPLIFY_AUTH) private auth: typeof Auth,
) {
this.authenticationSubject = new BehaviorSubject<boolean>(false);
}
public async signUp(user: IUser): Promise<any> {
// CHANGE TO this.auth
return this.auth.signUp({
username: user.name,
password: user.password,
attributes: {
email: user.email,
name: user.name,
},
});
}
public confirmSignUp(user: IUser): Promise<any> {
// CHANGE TO this.auth
return this.auth.confirmSignUp(user.name, user.code);
}
public async signIn(user: IUser): Promise<any> {
// CHANGE TO this.auth
const r = await this.auth.signIn(user.name, user.password);
console.log('signIn response', r);
this.authenticationSubject.next(true);
if (user) {
if (r.challengeName === 'NEW_PASSWORD_REQUIRED') {
this.router.navigateByUrl('/forgotpass');
} else {
this.router.navigateByUrl('/home');
}
}
}
}
Then in your test you need to provide both of them: the service and a mock token:
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [
AwsAmplifyAuthService, // <- add it
{
provide: AWS_AMPLIFY_AUTH, // <- add a mock
useValue: {
signIn: () => Promise.resolve({
challengeName: 'NEW_PASSWORD_REQUIRED',
}),
currentUserInfo: () => Promise.resolve('hello'),
},
},
],
}).compileComponents();
service = TestBed.inject(AwsAmplifyAuthService);
});
profit, now you can manipulate Auth
in your tests.