0

I'm using Angular's HttpClientTestingModule and HttpTestingController for mocking HTTP requests. The login method of my service AuthenticationService does two things:

  1. Makes a POST request to the server. If the credentials are valid, the server returns a User object.
  2. "Logs" in a user by saving the response in localStorage.

As a result, I want to have two tests for my login method:

  1. Test if the user is returned
  2. Test if localStorage has been set

I can include both tests in a single it block, but I don't want to do that. I want to have one assert per unit test.

If I split them into different tests (as shown in the example below), there is a lot of code repetition. Is there a better way to do this?

Here is the relevant section of the authentication.service.spec.ts file.

...

let authenticationService: AuthenticationService;
let httpTestingController: HttpTestingController;
let store = {};

// Create a mock user object for testing purposes
const mockUser: User = {
    name: 'name',
    email: 'email@example.com',
}

beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [AuthenticationService],
    });
    authenticationService = TestBed.inject(AuthenticationService);
    httpTestingController = TestBed.inject(HttpTestingController);

    // Create mock localStorage
    spyOn(localStorage, 'getItem').and.callFake(key => {
      return store[key];
    });
    spyOn(localStorage, 'setItem').and.callFake((key, value) => {
      return store[key] = value + '';
    });
    spyOn(localStorage, 'removeItem').and.callFake(key => {
      delete store[key];
    });
});

describe('login', () => {
    it('should return user', () => {
      authenticationService.login('name', 'email@example.com').subscribe(
        data => {
          expect(data).toEqual(mockUser);
        }
      )
      const req = httpTestingController.expectOne('http://example.com/login');
      expect(req.request.method).toEqual('POST');
      req.flush(mockUser);
    });

    it('should set localstorage', () => {
      authenticationService.login('name', 'email@example.com').subscribe(
        data => {
          expect(localStorage.getItem('theUser')).toEqual(JSON.stringify(mockUser));
        }
      )
      const req = httpTestingController.expectOne('http://example.com/login');
      expect(req.request.method).toEqual('POST');
      req.flush(mockUser);
    });

    afterEach(() => {
      httpTestingController.verify();
    });
  });
...
wiseindy
  • 19,434
  • 5
  • 27
  • 38

1 Answers1

1

I would do both tests in one:

it('should return user and set localStorage', () => {
      authenticationService.login('name', 'email@example.com').subscribe(
        data => {
          expect(data).toEqual(mockUser);
          expect(localStorage.getItem('theUser')).toEqual(JSON.stringify(mockUser));
        }
      );
      const req = httpTestingController.expectOne('http://example.com/login');
      expect(req.request.method).toEqual('POST');
      req.flush(mockUser);
    });

But I see you don't want to do that. You could try creating a re-usable function (called sendLoginResponse here).

describe('login', () => {
    const sendLoginResponse = () => {
      const req = httpTestingController.expectOne('http://example.com/login');
      expect(req.request.method).toEqual('POST');
      req.flush(mockUser);
    };
    it('should return user', () => {
      authenticationService.login('name', 'email@example.com').subscribe(
        data => {
          expect(data).toEqual(mockUser);
        }
      )
      sendLoginResponse();
    });

    it('should set localstorage', () => {
      authenticationService.login('name', 'email@example.com').subscribe(
        data => {
          expect(localStorage.getItem('theUser')).toEqual(JSON.stringify(mockUser));
        }
      )
      sendLoginResponse();
    });

    afterEach(() => {
      httpTestingController.verify();
    });
  });
AliF50
  • 16,947
  • 1
  • 21
  • 37
  • Yeah, seems like there is no alternate way to do this. I'll go ahead and create a common function then as you've mentioned. Thanks! – wiseindy Oct 15 '20 at 13:13
  • Follow up question: Would it make sense to include `sendLoginResponse()` in `afterEach`? – wiseindy Oct 15 '20 at 13:18
  • 1
    That could work but to be sincere with you I like the approach of how you have it in your question. It becomes much more clearer where I can focus on one `it` block and go through the arrange, action, assert pattern rather than going through re-usable functions. This thread should be a good read for you: https://stackoverflow.com/q/6453235/7365461 – AliF50 Oct 15 '20 at 14:18
  • 1
    That thread was very insightful. Thank you so much for sharing. I was struggling with this exact issue in my unit tests and the link makes everything much clearer. – wiseindy Oct 15 '20 at 15:07