Как и для любой внешней зависимости, нужно мокировать HTTP-backend, чтобы тесты могли симулировать взаимодействие с удалённым сервером. Библиотека @angular/common/http/testing предоставляет инструменты для захвата запросов, сделанных приложением, утверждений о них и мокирования ответов для эмуляции поведения вашего backend.
Библиотека тестирования рассчитана на паттерн, где приложение сначала выполняет код и делает запросы. Затем тест ожидает, что определённые запросы были или не были сделаны, выполняет утверждения против этих запросов и наконец предоставляет ответы, «сбрасывая» (flushing) каждый ожидаемый запрос.
Наконец, тесты могут проверить, что приложение не сделало неожиданных запросов.
Настройка для тестирования
Чтобы начать тестировать использование HttpClient, настройте TestBed и включите provideHttpClientTesting() в настройку теста. HttpClient предоставляется тестовым окружением Angular, а provideHttpClientTesting() настраивает его на использование тестового backend вместо реальной сети. Он также предоставляет HttpTestingController, который вы будете использовать для взаимодействия с тестовым backend, установки ожиданий о том, какие запросы были сделаны, и сброса ответов на эти запросы. HttpTestingController можно внедрить из TestBed после настройки.
TestBed.configureTestingModule({
providers: [
// ... other test providers
provideHttpClientTesting(),
],
});
const httpTesting = TestBed.inject(HttpTestingController);
Теперь когда ваши тесты делают запросы, они попадут в тестовый backend вместо обычного. Можно использовать httpTesting для утверждений об этих запросах.
Настройка HttpClient в тестах
Если тесту нужно настроить возможности HttpClient, например interceptor'ы, включите provideHttpClient(...) перед provideHttpClientTesting().
IMPORTANT: Помните, что нужно предоставлять provideHttpClient() перед provideHttpClientTesting(), потому что provideHttpClientTesting() перезапишет части provideHttpClient(). Обратный порядок может потенциально сломать тесты.
TestBed.configureTestingModule({
providers: [provideHttpClient(withInterceptors([authInterceptor])), provideHttpClientTesting()],
});
Ожидание и ответы на запросы
Например, можно написать тест, который ожидает GET-запрос и предоставляет mock-ответ:
TestBed.configureTestingModule({
providers: [ConfigService, provideHttpClientTesting()],
});
const httpTesting = TestBed.inject(HttpTestingController);
// Load `ConfigService` and request the current configuration.
const service = TestBed.inject(ConfigService);
const config$ = service.getConfig<Config>();
// `firstValueFrom` subscribes to the `Observable`, which makes the HTTP request,
// and creates a `Promise` of the response.
const configPromise = firstValueFrom(config$);
// At this point, the request is pending, and we can assert it was made
// via the `HttpTestingController`:
const req = httpTesting.expectOne('/api/config', 'Request to load the configuration');
// We can assert various properties of the request if desired.
expect(req.request.method).toBe('GET');
// Flushing the request causes it to complete, delivering the result.
req.flush(DEFAULT_CONFIG);
// We can then assert that the response was successfully delivered by the `ConfigService`:
expect(await configPromise).toEqual(DEFAULT_CONFIG);
// Finally, we can assert that no other requests were made.
httpTesting.verify();
ПРИМЕЧАНИЕ: expectOne завершится неудачей, если тест сделал более одного запроса, соответствующего заданным критериям.
Как альтернативу утверждению на req.method можно использовать расширенную форму expectOne, чтобы также сопоставить метод запроса:
const req = httpTesting.expectOne(
{
method: 'GET',
url: '/api/config',
},
'Request to load the configuration',
);
ПОЛЕЗНО: API ожиданий сопоставляются с полным URL запросов, включая любые query-параметры.
Последний шаг — проверка, что не осталось незавершённых запросов — достаточно распространён, чтобы перенести его в шаг afterEach():
afterEach(() => {
// Verify that none of the tests make any extra HTTP requests.
TestBed.inject(HttpTestingController).verify();
});
Обработка более одного запроса одновременно
Если нужно ответить на дублирующиеся запросы в тесте, используйте API match() вместо expectOne(). Он принимает те же аргументы, но возвращает массив совпадающих запросов. После возврата эти запросы удаляются из будущего сопоставления, и вы отвечаете за их flush и проверку.
const allGetRequests = httpTesting.match({method: 'GET'});
for (const req of allGetRequests) {
// Handle responding to each request.
}
Продвинутое сопоставление
Все функции сопоставления принимают функцию-предикат для пользовательской логики сопоставления:
// Look for one request that has a request body.
const requestsWithBody = httpTesting.expectOne((req) => req.body !== null);
Функция expectNone утверждает, что ни один запрос не соответствует заданным критериям.
// Assert that no mutation requests have been issued.
httpTesting.expectNone((req) => req.method !== 'GET');
Тестирование обработки ошибок
Следует тестировать ответы приложения, когда HTTP-запросы завершаются неудачей.
Ошибки backend
Чтобы тестировать обработку ошибок backend (когда сервер возвращает неуспешный код статуса), сбрасывайте запросы с ответом об ошибке, эмулирующим то, что вернул бы ваш backend при сбое запроса.
const req = httpTesting.expectOne('/api/config');
req.flush('Failed!', {status: 500, statusText: 'Internal Server Error'});
// Assert that the application successfully handled the backend error.
Сетевые ошибки
Запросы также могут завершаться неудачей из-за сетевых ошибок, которые проявляются как ошибки ProgressEvent. Их можно доставить методом error():
const req = httpTesting.expectOne('/api/config');
req.error(new ProgressEvent('network error!'));
// Assert that the application successfully handled the network error.
Тестирование Interceptor
Следует тестировать, что ваши interceptor'ы работают в желаемых обстоятельствах.
Например, приложению может требоваться добавлять токен аутентификации, сгенерированный сервисом, к каждому исходящему запросу. Это поведение можно обеспечить с помощью interceptor:
export function authInterceptor(
request: HttpRequest<unknown>,
next: HttpHandlerFn,
): Observable<HttpEvent<unknown>> {
const authService = inject(AuthService);
const clonedRequest = request.clone({
headers: request.headers.append('X-Authentication-Token', authService.getAuthToken()),
});
return next(clonedRequest);
}
Конфигурация TestBed для этого interceptor должна опираться на возможность withInterceptors.
TestBed.configureTestingModule({
providers: [
AuthService,
// Testing one interceptor at a time is recommended.
provideHttpClient(withInterceptors([authInterceptor])),
provideHttpClientTesting(),
],
});
HttpTestingController может получить экземпляр запроса, который затем можно проверить, чтобы убедиться, что запрос был изменён.
const service = TestBed.inject(AuthService);
const req = httpTesting.expectOne('/api/config');
expect(req.request.headers.get('X-Authentication-Token')).toEqual(service.getAuthToken());
Похожий interceptor можно реализовать с классовыми interceptor'ами:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private authService = inject(AuthService);
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const clonedRequest = request.clone({
headers: request.headers.append('X-Authentication-Token', this.authService.getAuthToken()),
});
return next.handle(clonedRequest);
}
}
Чтобы протестировать его, конфигурация TestBed вместо этого должна быть:
TestBed.configureTestingModule({
providers: [
AuthService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
// We rely on the HTTP_INTERCEPTORS token to register the AuthInterceptor as an HttpInterceptor
{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
],
});