Angular 4 unit testing with jasmine /karma with ht

2020-03-01 20:34发布

问题:

I have a service I want to unit test in angular 4 typescript jasmine.

Now, the http is doing a post , and it returns an identity, however.. it is not sending anything.

I want to just have good code coverage but i don't understand how to quite complete this mocking statement.

here is the method for http post in my service file

addSession() {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });

        return this.http.post(this.url, JSON.stringify({}), options)
            .map((response: Response) => response.json());

}

Then the SPEC FILE , which i don't get what to really test, i suppose faking that i received a number back from the service http post, the response should be something like 000000014

Spec

import { TrackerFormService } from './tracker-form.service'
import { Observable } from 'rxjs/Observable'

describe('TrackerFormService', () => {

    let trackerFormService: TrackerFormService,
        mockHttp;

    beforeEach(() => {
        mockHttp = jasmine.createSpyObj('mockHttp', ['get', 'post', 'put']
        )
        trackerFormService = new TrackerFormService(mockHttp);
    });

    describe('addSession', () => {

        it('add session ', () => {
              // how to test,  what to test?    
              // response , is a number?  how to mock/fake this?

        })

    })

})

回答1:

In order to achieve what you want, the mock you need is a simple function that returns the same as the POST would do normally; another thing is your test should not hit the server for real, so you would need something like this (you might need to add other dependencies):

import { HttpModule } from '@angular/http';
import { TrackerFormService } from './tracker-form.service'
import { Observable } from 'rxjs/Observable'

describe('TrackerFormService', () => {
// Mock the service like this and add all the functions you have in this fashion
let trackerFormService: TrackerFormService,
  mockService = {
    addSession: jasmine.createSpy('addSession').and.returnValue(Observable.of('your session object mock goes here'))
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpModule],
      providers: [{
        provide: TrackerFormService,
        useValue: mockService
      }]
    });
  });

  // Do this trick to inject the service every time, and just use `service` in your tests
  beforeEach(inject([TrackerFormService], (trackerFormService) => {
    service = trackerFormService;
  }));

  describe('addSession', () => {
    it('add session ', () => {
      let fakeResponse = null;

      // Call the service function and subscribe to it to catch the fake response coming from the mock.
      service.addSession().subscribe((value) => {
        // in here value will be whatever you put as returnValue (remember to keep the observable.of())
        fakeResponse = value;
      });

      // expects as in any test.
      expect(fakeResponse).toBeDefined();
      expect(fakeResponse).toBe('your session object mock goes here');
    });
  });
});


回答2:

With Angular 4.3 came the HttpClient service which replaces Http and provides an easier way to mock HTTP requests. It's well documented on the official page: https://angular.io/guide/http



回答3:

Well the way you did setup the test/mock you can fake the return of the post call and check that you got the result you expected. By doing so you will test that the mocked response will be properly converted by your map statement. With your spy you can also check how the post method was called. This will check if the options match what you expect.

But in my opinion that’s a rather complicated solution. I'd prefer to avoid mocks and spies by splitting the method so every method is just doing one thing. Because your addSession method is currently doing three different (yet logically dependent) things:

  1. create options for an addSession xhr call
  2. performs the call
  3. convert the response

If you break the method up in three you can easily test method #1 and #3 in separate tests and method #2 would only contain the call to the http library. This allows you to achieve the same test value as above without calling the http library.

Now what about method #2... it's still untested and in my opinion there is no reason to test it at all. Because you did not write that code. Also if you are using angulars http module I am sure they have solid unit tests themselves.

The response of your service should already be covered by an additional integration test, run less frequently checking the service api will still return what you expect.

If you really want that one line green in your code coverage, then you could optionally use a library called nock. Nock will intercept all xhr traffic your app will cause. In your test file you can map xhr requests to mocked responses with the nock object.

var scope = nock('http://myapp.iriscouch.com')
                .post('/users', {
                  username: 'pgte',
                  email: 'pedro.teixeira@gmail.com'
                })
                .reply(201, {
                  ok: true,
                  id: '123ABC',
                  rev: '946B7D1C'
                });

copied from: https://www.npmjs.com/package/nock

For reference and additional information about testing in general and how much to test i recommend watching "Budgeting Reality" by Justin Searls



回答4:

Sample test case for http service requests

describe('Forgot Password Controller', function () {
    var $controller,
        $httpBackend,
        $q,
        $rootScope,
        $state,
        controller,
        scope,
        accountProvider;

    beforeEach(module('app'));
    beforeEach(inject(function (_$injector_, _$controller_, _$rootScope_) {

        $controller = _$controller_;
        $rootScope = _$rootScope_;
        $httpBackend = _$injector_.get('$httpBackend');
        $state = _$injector_.get('$state');
        $q = _$injector_.get('$q');
        accountProvider = _$injector_.get('accountProvider');
        scope = _$rootScope_.$new();

        controller = $controller(app.controllers.forgotPassword, {
            $state: $state,
            accountProvider: accountProvider
        });
    }));

    afterEach(function () {
        $httpBackend.verifyNoOutstandingRequest();
        $httpBackend.verifyNoOutstandingExpectation();
    });

    describe('forgot password submission', function () {

        it('Can submit a forgot password request successfully', function () {
            $httpBackend.expectPOST("URL DOMAIN" + '/events/requestPasswordReset').respond(200);
            spyOn($state, 'go');
            controller.form = { emailAddress: 'aks@gmail.com' };

            controller.submit();

            expect(controller.submitting).toBe(true);

            $httpBackend.flush();

            expect(controller.submitting).toBe(false);
            expect($state.go).toHaveBeenCalledWith('login', { successMessage: 'An email sent to ' + controller.form.emailAddress + ' contains instructions for resetting your password.' });
        });

        it('Can handle when a user is not found when submitting a forgot password request', function () {
            $httpBackend.expectPOST(app.env.EDGE_SERVICE_PATH + '/events/requestPasswordReset').respond(404);
            spyOn($state, 'go');
            controller.form = { emailAddress: 'aks@gmail.com' };

            controller.submit();

            expect(controller.submitting).toBe(true);
            $httpBackend.flush();

            // We intentionally want to make it appear to the user that the password reset email was sent even when a user
            // does not exist, to help hide info about which users exist in the system
            expect(controller.submitting).toBe(false);
            expect($state.go).toHaveBeenCalledWith('login', { successMessage: 'An email sent to ' + controller.form.emailAddress + ' contains instructions for resetting your password.' });

        });

        it('Can handle unexpected errors from submitting a forgot password request', function () {
            $httpBackend.expectPOST("URL DOMAIN"  + '/events/requestPasswordReset').respond(500);

            controller.submit();
            $httpBackend.flush();

            expect(controller.errors.unexpectedError).toBe(true);
        });

        it('Can handle 422 validation errors from submitting a forgot password request', function () {
            var responseData = {
                fieldErrors: {
                    username: [{code: 'error'}, {code: 'required', message: 'This is required.'}]
                }
            };
            $httpBackend.expectPOST("URL DOMAIN" + '/events/requestPasswordReset').respond(422, responseData);

            controller.submit();
            $httpBackend.flush();

            expect(controller.errors.validationErrors).toBe(true);
            expect(controller.errors.fieldErrors).toEqual(responseData.fieldErrors);
        });

        it('Can handle 503 service unavailable from submitting a forgot password request', function () {
            $httpBackend.expectPOST("URL DOMAIN" + '/events/requestPasswordReset').respond(503);

            controller.submit();
            $httpBackend.flush();

            expect(controller.errors.serviceUnavailable).toBe(true);
        });

    });

});