Angular 2单元测试可观察错误(HTTP)

时间:2016-12-28 15:27:01

标签: unit-testing angular angular2-testing angular2-observables

我正在尝试为我的API服务编写单元测试,但在捕获HTTP错误时遇到一些问题。我正在关注此guide以及Angular2文档,因为指南在某些小区域(稍微)过时了。

所有单元测试都会与服务引发错误的单元测试分开(由于HTTP状态代码错误)。我可以通过退出response.ok来说明这一点。从我所读到的,这与单元测试没有异步执行有关,因此,不等待错误响应。但是,我不知道为什么会出现这种情况,因为我在async()方法中使用了beforeEach效用函数。

API服务

get(endpoint: string, authenticated: boolean = false): Observable<any> {
    endpoint = this.formatEndpoint(endpoint);
    return this.getHttp(authenticated) // Returns @angular/http or a wrapper for handling auth headers
        .get(endpoint)
        .map(res => this.extractData(res))
        .catch(err => this.handleError(err)); // Not in guide but should work as per docs
}
private extractData(res: Response): any {
    let body: any = res.json();
    return body || { };
}

private handleError(error: Response | any): Observable<any> {
    // TODO: Use a remote logging infrastructure
    // TODO: User error notifications
    let errMsg: string;
    if (error instanceof Response) {
        const body: any = error.json() || '';
        const err: string = body.error || JSON.stringify(body);
        errMsg = `${error.status} - ${error.statusText || ''}${err}`;
    } else {
        errMsg = error.message ? error.message : error.toString();
    }
    console.error(errMsg);
    return Observable.throw(errMsg);
}

错误单元测试

// Imports

describe('Service: APIService', () => {
    let backend: MockBackend;
    let service: APIService;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            providers: [
                BaseRequestOptions,
                MockBackend,
                APIService,
                {
                    deps: [
                        MockBackend,
                        BaseRequestOptions
                    ],
                    provide: Http,
                        useFactory: (backend: XHRBackend, defaultOptions: BaseRequestOptions) => {
                            return new Http(backend, defaultOptions);
                        }
                },
                {provide: AuthHttp,
                    useFactory: (http: Http, options: BaseRequestOptions) => {
                        return new AuthHttp(new AuthConfig({}), http, options);
                    },
                    deps: [Http, BaseRequestOptions]
                }
            ]
        });
        const testbed: any = getTestBed();
        backend = testbed.get(MockBackend);
        service = testbed.get(APIService);
    }));

    /**
     * Utility function to setup the mock connection with the required options
     * @param backend
     * @param options
     */
    function setupConnections(backend: MockBackend, options: any): any {
        backend.connections.subscribe((connection: MockConnection) => {
            const responseOptions: any = new ResponseOptions(options);
            const response: any = new Response(responseOptions);
            console.log(response.ok); // Will return false during the error unit test and true in others (if spyOn log is commented).
            connection.mockRespond(response);
        });
    }

    it('should log an error to the console on error', () => {
        setupConnections(backend, {
            body: { error: `Some strange error` },
            status: 400
        });
        spyOn(console, 'error');
        spyOn(console, 'log');

        service.get('/bad').subscribe(null, e => {
            // None of this code block is executed.
            expect(console.error).toHaveBeenCalledWith("400 - Some strange error");
            console.log("Make sure an error has been thrown");
        });

        expect(console.log).toHaveBeenCalledWith("Make sure an error has been thrown."); // Fails
    });

更新1

当我检查第一个回调时,response.ok未定义。这让我相信setupConnections实用程序中存在错误。

    it('should log an error to the console on error', async(() => {
        setupConnections(backend, {
            body: { error: `Some strange error` },
            status: 400
        });
        spyOn(console, 'error');
        //spyOn(console, 'log');

        service.get('/bad').subscribe(res => {
            console.log(res); // Object{error: 'Some strange error'}
            console.log(res.ok); // undefined
        }, e => {
            expect(console.error).toHaveBeenCalledWith("400 - Some strange error");
            console.log("Make sure an error has been thrown");
        });

        expect(console.log).toHaveBeenCalledWith("Make sure an error has been thrown.");
    }));

更新2

如果我没有在get方法中捕获错误,而是在map中明确地执行它,那么仍然有同样的问题。

get(endpoint: string, authenticated: boolean = false): Observable<any> {
    endpoint = this.formatEndpoint(endpoint);
    return this.getHttp(authenticated).get(endpoint)
        .map(res => {
            if (res.ok) return this.extractData(res);
            return this.handleError(res);
        })
        .catch(this.handleError);
}

更新3

经过一些讨论后this issue提交了

2 个答案:

答案 0 :(得分:4)

这是我的工作解决方案,与上述建议类似,但更清晰:

it('should log an error to the console on error', async(inject([AjaxService, MockBackend], (
    ajaxService: AjaxService, mockBackend: MockBackend) => {
    service = ajaxService;
    backend = mockBackend;
    backend.connections.subscribe((connection: MockConnection) => {
      const options: any = new ResponseOptions({
        body: { error: 'Some strange error' },
        status: 404
      });
      const response: any = new Response(options);
      connection.mockError(response);
    });
    spyOn(console, 'error');
    service.get('/bad').subscribe(res => {
      console.log(res); // Object{error: 'Some strange error'}
    }, e => {
      expect(console.error).toHaveBeenCalledWith('404 - Some strange error');
    });

  })));
  

参考完整的工作代码:

以下是所有可能的测试方案。 注意:不要担心 AjaxService 。它是我在角度http服务上的自定义包装器,它被用作拦截器。

<强> ajax.service.spec.ts

import { AjaxService } from 'app/shared/ajax.service';
import { TestBed, inject, async } from '@angular/core/testing';
import { Http, BaseRequestOptions, ResponseOptions, Response } from '@angular/http';
import { MockBackend, MockConnection } from '@angular/http/testing';

describe('AjaxService', () => {
  let service: AjaxService = null;
  let backend: MockBackend = null;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      providers: [
        MockBackend,
        BaseRequestOptions,
        {
          provide: Http,
          useFactory: (backendInstance: MockBackend, defaultOptions: BaseRequestOptions) => {
            return new Http(backendInstance, defaultOptions);
          },
          deps: [MockBackend, BaseRequestOptions]
        },
        AjaxService
      ]
    });
  }));

  it('should return mocked post data',
    async(inject([AjaxService, MockBackend], (
      ajaxService: AjaxService, mockBackend: MockBackend) => {
      service = ajaxService;
      backend = mockBackend;
      backend.connections.subscribe((connection: MockConnection) => {
        const options = new ResponseOptions({
          body: JSON.stringify({ data: 1 }),
        });
        connection.mockRespond(new Response(options));
      });

      const reqOptions = new BaseRequestOptions();
      reqOptions.headers.append('Content-Type', 'application/json');
      service.post('', '', reqOptions)
        .subscribe(r => {
          const out: any = r;
          expect(out).toBe(1);
        });
    })));

  it('should log an error to the console on error', async(inject([AjaxService, MockBackend], (
    ajaxService: AjaxService, mockBackend: MockBackend) => {
    service = ajaxService;
    backend = mockBackend;
    backend.connections.subscribe((connection: MockConnection) => {
      const options: any = new ResponseOptions({
        body: { error: 'Some strange error' },
        status: 404
      });
      const response: any = new Response(options);
      connection.mockError(response);
    });
    spyOn(console, 'error');
    service.get('/bad').subscribe(res => {
      console.log(res); // Object{error: 'Some strange error'}
    }, e => {
      expect(console.error).toHaveBeenCalledWith('404 - Some strange error');
    });

  })));

  it('should extract mocked data with null response',
    async(inject([AjaxService, MockBackend], (
      ajaxService: AjaxService, mockBackend: MockBackend) => {
      service = ajaxService;
      backend = mockBackend;
      backend.connections.subscribe((connection: MockConnection) => {
        const options = new ResponseOptions({
        });
        connection.mockRespond(new Response(options));
      });

      const reqOptions = new BaseRequestOptions();
      reqOptions.headers.append('Content-Type', 'application/json');
      service.get('test', reqOptions)
        .subscribe(r => {
          const out: any = r;
          expect(out).toBeNull('extractData method failed');
        });
    })));

  it('should log an error to the console with empty response', async(inject([AjaxService, MockBackend], (
    ajaxService: AjaxService, mockBackend: MockBackend) => {
    service = ajaxService;
    backend = mockBackend;
    backend.connections.subscribe((connection: MockConnection) => {
      const options: any = new ResponseOptions({
        body: {},
        status: 404
      });
      const response: any = new Response(options);
      connection.mockError(response);
    });
    spyOn(console, 'error');
    service.get('/bad').subscribe(res => {
      console.log(res); // Object{error: 'Some strange error'}
    }, e => {
      expect(console.error).toHaveBeenCalledWith('404 - {}');
    });

    // handle null response in error
    backend.connections.subscribe((connection: MockConnection) => {
      connection.mockError();
    });
    const res: any = null;
    service.get('/bad').subscribe(res, e => {
      console.log(res);
    }, () => {
      expect(console.error).toHaveBeenCalledWith(null, 'handleError method with null error response got failed');
    });

  })));

});

<强> ajax.service.ts

import { Injectable } from '@angular/core';
import { Http, Response, RequestOptionsArgs, BaseRequestOptions } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';

/**
 * Wrapper around http, use this for all http operations.
 * It has centralized error handling as well.
 * @export
 * @class AjaxService
 */
@Injectable()
export class AjaxService {
  /**
   * Creates an instance of AjaxService.
   * @param {Http} http
   *
   * @memberOf AjaxService
   */
  constructor(
    private http: Http,
  ) { }

  /**
   * Performs a request with get http method.
   *
   * @param {string} url
   * @param {RequestOptionsArgs} [options]
   * @returns {Observable<Response>}
   *
   * @memberOf AjaxService
   */
  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    options = this.getBaseRequestOptions(options);
    options = this.setHeaders(options);
    return this.http.get(url, options)
      .map(this.extractData)
      .catch(this.handleError);
  }

  /**
   * Performs a request with post http method.
   *
   * @param {string} url
   * @param {*} body
   * @param {RequestOptionsArgs} [options]
   * @returns {Observable<Response>}
   *
   * @memberOf AjaxService
   */
  post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
    options = this.getBaseRequestOptions(options);
    options = this.setHeaders(options);
    return this.http.post(url, body, options)
      .map(this.extractData)
      .catch(this.handleError);
  }

  /**
   * Util function to fetch data from ajax response
   *
   * @param {Response} res
   * @returns
   *
   * @memberOf AjaxService
   */
  private extractData(res: Response) {
    const body = res.json();
    const out = body && body.hasOwnProperty('data') ? body.data : body;
    return out;
  }

  /**
   * Error handler
   * Future Scope: Put into remote logging infra like into GCP stackdriver logger
   * @param {(Response | any)} error
   * @returns
   *
   * @memberOf AjaxService
   */
  private handleError(error: Response | any) {
    let errMsg: string;
    if (error instanceof Response) {
      const body = error.json() || '';
      const err = body.error || JSON.stringify(body);
      errMsg = `${error.status} - ${error.statusText || ''}${err}`;
    } else {
      errMsg = error.message ? error.message : error.toString();
    }
     console.error(errMsg);
    return Observable.throw(errMsg);
  }

  /**
   * Init for RequestOptionsArgs
   *
   * @private
   * @param {RequestOptionsArgs} [options]
   * @returns
   *
   * @memberOf AjaxService
   */
  private getBaseRequestOptions(options: RequestOptionsArgs = new BaseRequestOptions()) {
    return options;
  }

  /**
   * Set the default header
   *
   * @private
   * @param {RequestOptionsArgs} options
   * @returns
   *
   * @memberOf AjaxService
   */
  private setHeaders(options: RequestOptionsArgs) {
    if (!options.headers || !options.headers.has('Content-Type')) {
      options.headers.append('Content-Type', 'application/json');
    }
    return options;
  }

}

答案 1 :(得分:3)

  

据我所知,这与单元测试没有异步执行有关,因此,不等待错误响应。但是,我不知道为什么会出现这种情况,因为我在public Observable<CopyOnWriteArrayList<Entity>> getEntities() { 方法中使用了async()效用函数

您需要在测试用例中使用它(beforeEach)。 it所做的是创建一个测试区域,等待所有异步任务在完成测试(或测试区域,例如async)之前完成。

因此beforeEach中的async只是等待异步任务在退出之前在方法中完成。但beforeEach也需要同样的事情。

it

更新

除了遗失的it('should log an error to the console on error', async(() => { })) 之外,MockConnection似乎存在错误。如果您查看mockRespond,它始终会调用async,而不会考虑状态代码

next

他们有一个mockError(Error)方法,即调用mockRespond(res: Response) { if (this.readyState === ReadyState.Done || this.readyState === ReadyState.Cancelled) { throw new Error('Connection has already been resolved'); } this.readyState = ReadyState.Done; this.response.next(res); this.response.complete(); }

error

但这不会调用允许您传递mockError(err?: Error) { // Matches ResourceLoader semantics this.readyState = ReadyState.Done; this.response.error(err); } 。这与真实XHRConnection的工作方式不一致,后者会检查状态,并通过ResponseResponse发送next,但是error相同}}

Response

对我来说听起来像个错误。你应该报告的事情。他们应该允许您在response.ok = isSuccess(status); if (response.ok) { responseObserver.next(response); // TODO(gdi2290): defer complete if array buffer until done responseObserver.complete(); return; } responseObserver.error(response); 中发送Response,或者在mockError中对mockRespond进行同样的检查。

更新(通过OP)S​​etupConnections()

当前解决方案

XHRConnection