Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular6 how to test a sequence of http requests

I've created a service to getting user information from django rest auth. So I need 2 separate requests. one for getting auth token and one for getting user info.

In the userService service, I have a method called login that calls 2 other methods. Each of them sends a http request to a different url. For testing the behavior of login, I need to mock the requests of that 2 methods. First method returns a Promise that includes authentication key, and second method returns a Promise that includes user object. Here is my code in the service class:

public getAuthToken(identifier: string, password: string) {
  const requestBody = is_valid_email(identifier) ? {email: identifier, password: password} :
                                                 {username: identifier, password: password};
  let savedToken = getFromStorage('auth');
  if (savedToken) {
    try {
      savedToken = JSON.parse(savedToken);
    } catch (e) {
      savedToken = null;
    }
  }
  return new Promise((resolve, reject) => {
    if (savedToken) {
      resolve(savedToken);
    } else {
      this.http.post<string>(APIUrlSolver.login, requestBody).subscribe(data => {
        const dataObj = JSON.parse(data);
        UserService._auth_token = dataObj['key'];
        resolve(dataObj['key']);
      }, error1 => {
        // Rejection code. removed for better reading
      });
    }
  });
}

public getUserProfile(): Promise<UserModel> {
  return new Promise<UserModel>((resolve, reject) => {
    this.http.get(APIUrlSolver.user).subscribe((data: string) => {
      const jsonData = JSON.parse(data);
      const userObj = new UserModel(jsonData.username, jsonData.email, jsonData.first_name, jsonData.last_name, jsonData.phone,
        jsonData.birth_date);
      UserService._user = userObj;
      resolve(userObj);
    }, error1 => {
      // Rejection code. removed for better reading
    });
  });
}

public login(identifier: string, password: string) {
  return new Promise((resolve, reject) => {
    this.getAuthToken(identifier, password).then(key => {
      this.getUserProfile().then(user => {
        // Will resolve user object
      }).catch(error => {
        UserService._auth_token = undefined;
        reject(error);
      });
    }).catch(reason => {
      UserService._auth_token = undefined;
      reject(reason);
    });
  });
}

I've tried to test this method with following code:

      describe('userService', () => {
  let userService: UserService;
  let httpClient: HttpTestingController;
  const mockedUser = new UserModel();
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    userService = TestBed.get(UserService);
    httpClient = TestBed.get(HttpTestingController);
  });

  afterEach(() => {
    httpClient.verify();
  });

    it('#login', () => {
    const authResponse = {key: '74f0d5ffb992f5f49533d25c686f36414e64482c'};
    const response = {username: 'daaaaaaab', email: '[email protected]', first_name: 'test', last_name: 'test', phone: '09123657894',
      birth_date: '2018-07-31'};
    const expectedUser = new UserModel(response.username, response.email, response.first_name, response.last_name, response.phone,
      response.birth_date);

    userService.login('identifier', 'password').then(user => {
      expect(user).toEqual(expectedUser);
      expect(userService.user).toEqual(expectedUser);
      });

    const req = httpClient.expectOne(APIUrlSolver.login);   // This response works correct
    expect(req.request.method).toBe('POST');
    req.flush(JSON.stringify(authResponse));

    const userReq = httpClient.expectOne(APIUrlSolver.user);    // I get error here
    expect(req.request.method).toBe('GET');
    userReq.flush(JSON.stringify(response));
    });

     });

But this code will always faild on userReq. because expectOne raises:

 Error: Expected one matching request for criteria "Match URL: /user/user/", found none.

The real question is how I can test this sequence of http requests Because HttpClientTestingModule did not work

like image 545
Mr Alihoseiny Avatar asked Sep 07 '18 09:09

Mr Alihoseiny


1 Answers

You can try using TypeMoq for mocking your HTTP client. The following code should send you in the right direction. It is not tested, though.

describe('user service', () => {

  const tokenData = { key: 'asdasd' };
  const userResponse = { name: 'user', /* ... and so on */ };

  let mockHttpClient: TypeMoq.IMock<HttpClient>;
  let mockUserService: TypeMoq.IMock<UserService>;

  beforeAll(async(() => {

    mockHttpClient = TypeMoq.Mock.ofType<HttpClient>();
    mockHttpClient.setup(x => 
      x.get(TypeMoq.It.isValue(APIUrlSolver.login)).returns(() => of(tokenData))
    );
    mockHttpClient.setup(x => 
      x.get(TypeMoq.It.isValue(APIUrlSolver.user)).returns(() => of(userResponse))
    );

    mockUserService = TypeMoq.Mock.ofType<UserService>();

    TestBed
      .configureTestingModule({
        declarations: [],
        providers: [
          { provide: HttpClient, useValue: mockHttpService.object},
          { provide: UserService, useValue: mockUserService.object},
        ]
      })
      .compileComponents();
  }));

  let userService: UserService;

  beforeEach(async(() => {
    userService = TestBed.get(UserService);
  });

  it('login flow', async () => {
     const user = await userService.login('identifier', 'password');
     mockUserService.verify(x => x.getToken, TypeMoq.Times.once());
     mockUserService.verify(x => x.getUserProfile, TypeMoq.Times.once());
     mockHttpService.verify(x => x.get, TypeMoq.Times.exactly(2));
     // ... any other assertions
  });
});

Hope this helps a little :-)


EDIT

Since you want to use the in-built stuff, I'd recommend switching your logic to an observable flow. Your problem might be caused by you having all the calls promisified, not using the observable api.

This is how I would write it - you can try it out and see if it helps :-)

public getAuthToken(identifier: string, password: string): Observable<string> {

  let savedToken = getSavedToken();
  if(savedToken) return of(savedToken);

  return this.http.post<string>(APIUrlSolver.login, getRequestBody(identifier, password)).pipe(
    // Instead of this mapping you can use `this.http.post<YourDataStructure>`
    // and the JSON deserialization will be done for you
    map(data => JSON.parse(data)),
    map(data => UserService._auth_token = data['key']),
    // You can either handle the error here or let it fall through and handle it later
    catchError(err => /* error handling code */)
  );

public getUserProfile(): Observable<UserModel> {
  return this.http.get(APIUrlSolver.user).pipe(
    // Again, if your response JSON already looks like the `UserModel`,
    // you can simply write `this.http.get<UserModel>` ...
    map(data => {
      let d = JSON.parse(data);
      return UserService._user = new UserModel(d.username, d.email, d.first_name, d.last_name, d.phone, d.birth_date);
    }),
    catchError(err => /* as stated before... */)
  );

public login(identifier: string, password: string) {
  return this.getAuthToken(identifier, password).pipe(
    switchMap(key => this.getUserProfile()),
    catchError(err => /* Your login-wide error handler */)
  );

Then in your test, you just invoke the login method and subscribe to the result (instead of using then - it's not a promise anymore).

Your other test setup looks just fine - the problem is using promises in my opinion.

like image 85
Heehaaw Avatar answered Oct 21 '22 10:10

Heehaaw