简体   繁体   English

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

[英]Angular 2 Unit Testing Observable Errors (HTTP)

I am trying to write unit tests for my API service but have some trouble catching HTTP errors. 我正在尝试为我的API服务编写单元测试,但是在捕获HTTP错误时遇到了一些麻烦。 I am following this guide along with the Angular2 docs since the guide is (slightly) out of date in some minor areas. 我在Angular2文档中关注本指南 ,因为该指南在某些次要领域(略)过时了。

All unit tests pass apart from those where an error is thrown by the service (due to error HTTP status code). 所有单元测试都通过了服务抛出错误的测试(由于错误HTTP状态代码)。 I can tell this by logging out response.ok . 我可以通过注销response.ok来说明这一点。 From what i've read this has something to do with the unit tests not executing asynchronously, hence, not waiting for the error response. 从我读到的内容来看,这与单元测试不是异步执行有关,因此不必等待错误响应。 However, I have no idea why this is the case here since I have used the async() utility function in the beforeEach method. 但是,我不知道为什么会出现这种情况,因为我在beforeEach方法中使用了async()实用程序函数。

API Service 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);
}

Error unit test 错误单元测试

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

Update 1 更新1

when I check the first callback, response.ok is undefined. 当我检查第一个回调时,response.ok是未定义的。 This leads me to believe that there is something wrong in the setupConnections utility. 这使我相信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.");
    }));

Update 2 更新2

If, instead of catching errors in the get method I do it explicitly in map then still have same problem. 如果不是在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);
}

Update 3 更新3

After some discussion this issue submitted 经过一番讨论, 该问题提交了

Here is my working solution which is similar to above suggestions but with more clarity: 这是我的工作解决方案,与上述建议类似,但更加清晰:

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

  })));

Reference full working code: 参考完整的工作代码:

Below are all possible test scenarios. 以下是所有可能的测试方案。 Note: Don't worry about AjaxService . 注意:不用担心AjaxService It's my custom wrapper on angular http service which is being used as a interceptor. 这是我在有角度的http服务上用作拦截器的自定义包装。

ajax.service.spec.ts 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 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;
  }

}

From what i've read this has something to do with the unit tests not executing asynchronously, hence, not waiting for the error response. 从我读到的内容来看,这与单元测试不是异步执行有关,因此不必等待错误响应。 However, I have no idea why this is the case here since I have used the async() utility function in the beforeEach method 但是,我不知道为什么会出现这种情况,因为我在beforeEach方法中使用了async()实用程序函数

You need to use it in the test case (the it ). 您需要在测试用例( it )中使用它。 What async does is create an test zone that waits for all async tasks to complete before completing the test (or test area, eg beforeEach ). async作用是创建一个测试区域,等待所有异步任务完成,然后再完成测试(或测试区域,例如beforeEach )。

So the async in the beforeEach is only waiting for the async tasks to complete in the method before exiting it. 因此, asyncbeforeEach退出之前只等待异步任务完成的方法。 But the it also needs that same thing. 但是it也需要同样的东西。

it('should log an error to the console on error', async(() => {

}))

UPDATE 更新

Aside from the missing async , there seems to be a bug with the MockConnection . 除了缺少asyncMockConnection似乎还有一个错误。 If you look at the mockRespond , it always calls next , not taking into consideration the status code 如果您看一下mockRespond ,它将始终调用next ,而不考虑状态码

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

They have a mockError(Error) method, which is what calls error 他们有一个mockError(Error)方法,这就是所谓的error

mockError(err?: Error) {
  // Matches ResourceLoader semantics
  this.readyState = ReadyState.Done;
  this.response.error(err);
}

but this does not call allow you to pass a Response . 但这并不允许您传递Response This is inconsistent with how the real XHRConnection works, which checks for the status, and sends the Response either through the next or error , but is the same Response 这与实际XHRConnection工作方式不一致,后者会检查状态,并通过nexterror发送Response ,但是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);

Sounds like a bug to me. 对我来说听起来像个虫子。 Something you should probably report. 您可能应该报告的内容。 They should allow you to either send the Response in the mockError or do the same check in the mockRespond that they do in the XHRConnection . 他们应该让您无论是发送ResponsemockError或做在相同的检查mockRespond他们在做XHRConnection

Updated (by OP) SetupConnections() 更新(通过OP)S​​etupConnections()

Current solution 当前解决方案

function setupConnections(backend: MockBackend, options: any): any {
    backend.connections.subscribe((connection: MockConnection) => {
        const responseOptions: any = new ResponseOptions(options);
        const response: any = new Response(responseOptions);

        // Have to check the response status here and return the appropriate mock
        // See issue: https://github.com/angular/angular/issues/13690
        if (responseOptions.status >= 200 && responseOptions.status <= 299)
            connection.mockRespond(response);
        else
            connection.mockError(response);
    });
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM