'AWS Amplify Cognito Angular Unit testing

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.



Solution 1:[1]

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.

Solution 2:[2]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 AliF50
Solution 2