Skip to end for stackblitz example
Problem:
Display list from api example
<ul>
<li *ngFor="let user of users"> {{user.name}}-{{user.id}}</li>
</ul>
Load users from api "https://jsonplaceholder.typicode.com/users"
constructor(http: HttpClient){
http.get<[User]>('https://jsonplaceholder.typicode.com/users').subscribe( res => {
this.users = res;
});
}
Implementation with mock
Why:
Lets say backend is 2ms slower or unstable or just temporary unavailable or client vpn is slow. Mock backend for trouble free isolated angular app only testing. It can be used just for faster development or for e2e testing.
Alternatives:
Json server
1. HttpMockRequestInterceptor
Usages of http interceptors are many (modifing headers, caching etc) but def we could use it to implement mocked http calls :)
First we will write HttpMockRequestInterceptor class that will catch all http calls from the app and return guess what- json responses loaded with imports (this is angular7 feature - you might need to modify tsconfig.ts with resolveJsonModule, esModuleInterop and allowSyntheticDefaultImports set to true).
import { Injectable, Injector } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
@Injectable()
export class HttpMockRequestInterceptor implements HttpInterceptor {
constructor(private injector: Injector) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Intercepted httpCall' + request.url);
return next.handle(request);
}
}
2. module.ts
We register interceptor inside module.ts
@NgModule({
imports: [ ... ],
declarations: [ .. ],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: HttpMockRequestInterceptor,
multi: true
}
],
bootstrap: [ AppComponent ]
})
Now, problem with this is that this mock interceptor will always intercept stuff and we dont want that. We will deal with MOCK serve configuration with environments later. Lets go back to interceptor and return json for "https://jsonplaceholder.typicode.com/users" url:
3 Match URL and return JSON
We will use simple json file named users.json
[
{
"name": "John",
"id": 666
},
{
"name": "Ron",
"id": 55
},
{
"name": "Ron",
"id": 345
},
{
"name": "Ron",
"id": 645654
}
]
Lets import that json file and return it as sucessfull 200 HttpResponse.
import { Injectable, Injector } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import * as users from './users.json';
const urls = [
{
url: 'https://jsonplaceholder.typicode.com/users',
json: users
}
];
@Injectable()
export class HttpMockRequestInterceptor implements HttpInterceptor {
constructor(private injector: Injector) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
for (const element of urls) {
if (request.url === element.url) {
console.log('Loaded from json : ' + request.url);
return of(new HttpResponse({ status: 200, body: ((element.json) as any).default }));
}
}
console.log('Loaded from http call :' + request.url);
return next.handle(request);
}
}
Couple of things to note:
. We match url only, not method.
. Import is used to load json files, not require or http get.
. Simple console logging is there because matched http calls are not going to be visible in Brwoser Networks tab- they are intercepted.
4 load mockinterceptor with start:mock script
Add additional property inside environment.ts named mock that we will need later in app.module.ts and create additional file named environment.mock.ts:
export const environment = {
production: true,
mock: true
};
Now we need to add mock config inside angular.ts file in two places:
demo/architect/build/configurations
"mock": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.mock.ts"
}
]
}
And also inside serve:
demo/architect/serve
"mock": {
"browserTarget": "demo:build:mock"
}
Lovely jubbly, now we can based on script run app with mocked or real backend (localhost or server hosted).
package.json new script:
"start:mock": "ng serve --configuration=mock",
and Final step: conditional loading of interceptors inside app.module.ts based on mock flag.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HelloComponent } from './hello.component';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { environment } from '../environments/environment';
import { HttpRequestInterceptor } from './interceptor';
import { HttpMockRequestInterceptor } from './interceptor.mock';
export const isMock = environment.mock;
@NgModule({
imports: [ BrowserModule, FormsModule, HttpClientModule ],
declarations: [ AppComponent, HelloComponent ],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: isMock ? HttpMockRequestInterceptor : HttpRequestInterceptor,
multi: true
}
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Run "npm run start:mock" script to mock your backend.
Enjoy.
Here is Stackblitz demo as well...
Top comments (6)
Hi sandiz,
thank you for this interesting article! A small addon to your code:
Unless you do not need to intercept the standard http requests (to add a console log by using the HttpRequestInterceptor) you can modify your app.module.ts this way:
// ...
providers: [
...isMock ? [{
provide: HTTP_INTERCEPTORS,
useClass: HttpMockRequestInterceptor,
multi: true
}] : []
],
// ...
This assures that no interceptor is loaded in non-mock environments.
Your solution helped me with a cypress test. I was able to mock out the authentication without re-writing the code or adding hard-coded variables.
Very interesting article! thanks for this!
Hi Sanidz, I was wondering if that would also intercept a non-angular request. I would like to mock a Google Location API to save $$ when running e2e test.
Thanks!
Well, it is just a regular Angular Service, so it can intercept all requests coming from that app.
However if those requests are coming from another app you could just:
1 start local json server and mock needed google.apis
2 edit google api urls to hit new local mocked ones or if not possible
2.a edit hosts file so that your local json server acts and responds to all existing and real google api urls.
github.com/typicode/json-server
It also depends which e2e tests, cypress for example has full suport for endpoint mocking and could mock all requests out of the box :)
Hi sanidz, I found that with Angular 9/Ivy the prod build will still contain the mock data/interceptor - did you find any way to solve this?