使用 jasmine / karma 进行 Angular 4 单元测试和 http post mocking - 如何修复

Posted

技术标签:

【中文标题】使用 jasmine / karma 进行 Angular 4 单元测试和 http post mocking - 如何修复【英文标题】:Angular 4 unit testing with jasmine /karma with http post mocking - how to fix 【发布时间】:2018-03-07 08:01:12 【问题描述】:

我有一项服务,我想在 Angular 4 typescript jasmine 中进行单元测试。

现在,http 正在执行 post ,它返回一个身份,但是......它没有发送任何东西。

我只想拥有良好的代码覆盖率,但我不明白如何完全完成这个模拟语句。

这是我的服务文件中http post的方法

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());


然后是 SPEC FILE ,我没有得到真正测试的内容,我想假装我从服务 http 帖子中收到了一个数字,响应应该类似于 000000014

规格

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】:

Angular 4.3 出现了 HttpClient 服务,它取代了 Http 并提供了一种更简单的方法来模拟 HTTP 请求。官方页面上有详细记录:https://angular.io/guide/http

【讨论】:

我已经在使用另一个,我在问如何专门模拟我拥有的东西。您所说的相当于 1. in .net core 或 c# 7 ,您现在可以做 XYZ,非常好或 2. 在 ES6 中,现在好多了... 如果我做对了,你想在 TrackerFormService 的测试中模拟 TrackerFormService.addSession() 方法,这在我看来没有意义,因为你想测试真正的实现而不是模拟。 它从响应中返回一个类似 000000015 的数字,所以我确信人们已经嘲笑/伪造了 http post 服务的过程。我可以找到示例,但它们非常具体,以至于试图弄清楚如何模拟/伪造我的特定测试是我想要的。我不声称知道该怎么做,所以我在问这个问题【参考方案2】:

http 服务请求的示例测试用例

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);
        );

    );

);

【讨论】:

这不是 angular 4 解决方案。 是的,我在这里提供有关如何为rest apis编写测试用例的想法。 这将如何帮助在 ng 中模拟服务??【参考方案3】:

为了实现你想要的,你需要的 mock 是一个简单的函数,它返回的结果与 POST 正常的返回值相同;另一件事是你的测试不应该真正到达服务器,所以你需要这样的东西(你可能需要添加其他依赖项):

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');
    );
  );
);

【讨论】:

【参考方案4】:

好吧,您设置测试/模拟的方式可以伪造 post call 的返回并检查您是否得到了预期的结果。通过这样做,您将测试模拟的响应是否会被您的 map 语句正确转换。通过您的间谍,您还可以检查 post 方法是如何被调用的。这将检查选项是否符合您的预期。

但在我看来,这是一个相当复杂的解决方案。我宁愿通过拆分方法来避免模拟和间谍,所以每个方法都只是做一件事。因为您的 addSession 方法当前正在执行三个不同(但在逻辑上依赖)的事情:

    为 addSession xhr 调用创建选项 执行呼叫 转换响应

如果您将方法分成三部分,您可以轻松地在单独的测试中测试方法 #1 和 #3,而方法 #2 将只包含对 http 库的调用。这允许您在不调用 http 库的情况下获得与上述相同的测试值。

现在方法#2 怎么样......它仍然未经测试,我认为根本没有理由对其进行测试。因为您没有编写该代码。此外,如果您使用 angulars http 模块,我相信他们自己也有可靠的单元测试。

您的服务的响应应该已经被额外的集成测试覆盖,运行频率较低,检查服务 api 是否仍会返回您期望的内容。

如果您真的希望代码覆盖中的那一行是绿色的,那么您可以选择使用一个名为 nock 的库。 Nock 将拦截您的应用程序将导致的所有 xhr 流量。在您的测试文件中,您可以使用 nock 对象将 xhr 请求映射到模拟响应。

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

复制自:https://www.npmjs.com/package/nock

有关一般测试以及测试量的参考和其他信息,我建议观看 Justin Searls 的“预算现实”

【讨论】:

以上是关于使用 jasmine / karma 进行 Angular 4 单元测试和 http post mocking - 如何修复的主要内容,如果未能解决你的问题,请参考以下文章

安装和使用Karma-Jasmine进行自动化测试

markdown 使用Karma和Jasmine进行测试 - 介绍

使用 Angular 和 Jasmine/Karma 的私有方法进行测试和代码覆盖

使用 jasmine 和 karma 进行单元测试时形成数组错误

使用 Jasmine 和 Karma 进行角度单元测试时出错

为啥使用 jasmine 对 Node typescript 项目进行 Karma 单元测试会显示包含依赖项的覆盖范围?