测试HTTP请求

服务,由于它们执行异步任务的性质。 当我们进行HTTP请求时,我们使用异步方式,以免阻止应用程序的其余部分执行其操作。我们之前了解了一些组件的异步测试,幸运的是,很多这方面的知识也带到了测试异步服务中。

测试这种服务的基本策略是验证请求的内容(正确的URL),并确保我们模拟到服务中的数据通过正确的方法正确返回。

让我们来看一些代码:

wikisearch.ts

import {Http} from '@angular/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import 'rxjs/add/operator/map'
@Injectable()
export class SearchWiki {
  constructor (private http: Http) {}
  search(term: string): Observable<any> {
    return this.http.get(
      'https://en.wikipedia.org/w/api.php?' +
      'action=query&list=search&srsearch=' + term
    ).map((response) => response.json());
  }
  searchXML(term: string): Observable<any> {
    return this.http.get(
      'https://en.wikipedia.org/w/api.php?' +
      'action=query&list=search&format=xmlfm&srsearch=' + term
    );
  }
}

这里是一个基本的服务。 它将使用搜索项查询维基百科,并返回一个带有查询结果的Observable。 搜索功能将使用提供的术语进行GET请求,searchXML方法将执行相同的操作,但请求响应为XML而不是JSON。 正如你可以看到,它取决于HTTP模块向wikipedia.org发出请求。

我们的测试策略是检查服务是否已请求正确的网址,一旦我们回复了模拟数据,我们就要验证它是否返回相同的数据。

使用MockBackend测试HTTP请求

要对我们的服务进行单元测试,我们不想发出实际的HTTP请求。 为了实现这一点,我们需要模拟我们的HTTP服务。 Angular 2为我们提供了一个MockBackend类,它可以被配置为提供对我们的请求的模拟响应,而不实际做出网络请求。

然后可以将配置的MockBackend注入到HTTP中,因此对服务的任何调用(例如http.get)将返回我们预期的数据,从而允许我们隔离真实网络流量测试服务。

wikisearch.spec.ts

import {
  fakeAsync,
  inject,
  TestBed
} from '@angular/core/testing';
import {
  HttpModule,
  XHRBackend,
  ResponseOptions,
  Response,
  RequestMethod
} from '@angular/http';
import {
  MockBackend,
  MockConnection
} from '@angular/http/testing/mock_backend';
import {SearchWiki} from './wikisearch.service';
const mockResponse = {
  "batchcomplete": "",
  "continue": {
    "sroffset": 10,
    "continue": "-||"
  },
  "query": {
    "searchinfo": {
      "totalhits": 36853
    },
    "search": [{
      "ns": 0,
      "title": "Stuff",
      "snippet": "<span></span>",
      "size": 1906,
      "wordcount": 204,
      "timestamp": "2016-06-10T17:25:36Z"
    }]
  }
};
describe('Wikipedia search service', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpModule],
      providers: [
        {
          provide: XHRBackend,
          useClass: MockBackend
        },
        SearchWiki
      ]
    });
  });
  it('should get search results', fakeAsync(
    inject([
      XHRBackend,
      SearchWiki
    ], (mockBackend: XHRBackend, searchWiki: SearchWiki) => {
      const expectedUrl = 'https://en.wikipedia.org/w/api.php?' +
        'action=query&list=search&srsearch=Angular';
      mockBackend.connections.subscribe(
        (connection: MockConnection) => {
          expect(connection.request.method).toBe(RequestMethod.Get);
          expect(connection.request.url).toBe(expectedUrl);
          connection.mockRespond(new Response(
            new ResponseOptions({ body: mockResponse })
          ));
        });
      searchWiki.search('Angular')
        .subscribe(res => {
          expect(res).toEqual(mockResponse);
        });
    })
  ));
  it('should set foo with a 1s delay', fakeAsync(
    inject([SearchWiki], (searchWiki: SearchWiki) => {
      searchWiki.setFoo('food');
      tick(1000);
      expect(searchWiki.foo).toEqual('food');
    })
  ));
});

View Example

我们使用inject注入SearchWiki服务和MockBackend到我们的测试。然后,我们使用对fakeAsync的调用封装我们的整个测试,这将用于控制SearchWiki服务的异步行为以进行测试。

接下来,我们订阅来自我们后端的任何传入连接。这使我们可以访问一个对象MockConnection ,它允许我们配置我们想要从我们的后端发送的响应,以及测试任何来自我们正在测试的服务的请求。

在我们的示例中,我们要验证SearchWiki的搜索方法是否向正确的URL发出GET请求。这是通过查看当我们的SearchWiki服务连接到我们的模拟后端时获得的请求对象来完成。分析request.url属性,我们可以看到它的值是否是我们期望的值。这里我们只检查URL,但在其他情况下,我们可以看到是否已设置某些头,或者是否已发送某些POST数据。

现在,使用MockConnection对象,我们模拟了一些任意数据。我们创建一个新的ResponseOptions对象,我们可以配置我们的响应的属性。这遵循常规Angular 2响应类的格式。在这里,我们只需将body属性设置为您可能从维基百科中看到的基本搜索结果集。我们还可以设置Cookie,HTTP标头等等,或将状态值设置为非200状态,以测试我们的服务如何响应错误。一旦我们配置了ResponseOptions,我们创建一个Respond对象的新实例,并告诉我们后端通过调用.mockRespond开始将其用作响应。

可以使用多个响应。假设您的服务有两个可能的GET请求 - 一个用于/api/users,另一个用于/api/users/1。这些请求中的每一个具有不同的对应的模拟数据集合。当通过MockBackend订阅接收新的连接时,您可以检查请求的是什么类型的URL,并使用任何一组模拟数据进行响应。

最后,我们可以通过调用和订阅结果来测试SearchWiki服务的搜索方法。一旦我们的搜索过程完成,我们检查结果对象,看看它是否包含相同的数据,我们mock到我们的后端。如果是,那么恭喜你,你的测试已经通过。

在应该设置foo与1s延迟测试,你会注意到,我们调用tick(1000)模拟1秒的延迟。

替代HTTP Mock策略

使用MockBackend的一个替代方法是创建我们自己的轻模拟。 这里我们创建一个对象,然后告诉TypeScript使用类型断言将其视为Http 。 然后我们为它的get方法创建一个监测器,并返回一个类似于真正的Http服务的observable 。

此方法仍然允许我们检查服务是否已请求正确的URL,并返回预期的数据。

wikisearch.spec.ts

import {
  fakeAsync,
  inject,
  TestBed
} from '@angular/core/testing';
import {
  HttpModule,
  Http,
  ResponseOptions,
  Response
} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import {SearchWiki} from './wikisearch.service';
const mockResponse = {
  "batchcomplete": "",
  "continue": {
    "sroffset": 10,
    "continue": "-||"
  },
  "query": {
    "searchinfo": {
      "totalhits": 36853
    },
    "search": [{
      "ns": 0,
      "title": "Stuff",
      "snippet": "<span></span>",
      "size": 1906,
      "wordcount": 204,
      "timestamp": "2016-06-10T17:25:36Z"
    }]
  }
};
describe('Wikipedia search service', () => {
  let mockHttp: Http;
  beforeEach(() => {
    mockHttp = { get: null } as Http;
    spyOn(mockHttp, 'get').and.returnValue(Observable.of({
      json: () => mockResponse
    }));
    TestBed.configureTestingModule({
      imports: [HttpModule],
      providers: [
        {
          provide: Http,
          useValue: mockHttp
        },
        SearchWiki
      ]
    });
  });
  it('should get search results', fakeAsync(
    inject([SearchWiki], searchWiki => {
      const expectedUrl = 'https://en.wikipedia.org/w/api.php?' +
        'action=query&list=search&srsearch=Angular';
      searchWiki.search('Angular')
        .subscribe(res => {
          expect(mockHttp.get).toHaveBeenCalledWith(expectedUrl);
          expect(res).toEqual(mockResponse);
        });
    })
  ));
});

View Example

测试JSONP和XHR后端

一些服务利用JSONP或XHR模块获取数据,而不是传统的HTTP模块。 我们使用相同的策略来测试这些服务 - 创建一个模拟后端,初始化服务,并测试,看看我们的服务做出的请求是否正确,以及如果通过后端返回的数据成功地用于该服务。 幸运的是,依赖于XHR模块的服务的测试方式与使用HTTP模块的服务完全相同。 唯一的区别是在哪个类用于模拟后端。 在使用HTTP模块的服务中,用MockBackend类;在使用XHR的服务中,使用XHRBackend代替。 其他一切都保持不变

不幸的是,使用JSONP模块的服务使用明显不同的类来模拟后端。 MockBrowserJsonp类用于此场景。