-
Notifications
You must be signed in to change notification settings - Fork 0
task#22 use a mock server
The following tasks #22, #23 and #24 are very large and partly complex. In order to NOT loose to much time with implementation you can copy-paste the solution or compare it with the branch in this repository:
https://github.com/mbachmann/angular-tutorial-2020/tree/task%2322-use-a-fake-server
A mock backend is used for doing backendless development in Angular, which allows you to demo your code without the need of creating a backend server api. It's perfect when you're developing a front end before the backend is available.
The so far used test-aution-api is a simple mock server to handle get-requests for autions and auctions/:id.
For the new login and registration requirements we need a more sophisticated mock server,
which supports the http verbs post, put and delete.
The use of the mock server (enabled or disabled) is controlled by the environments in the src/environments folder.
For creating the mock server, we firstly need to refactor the strucure a bit. Then we create the mock-server it self and finally we create environments to use the mock-server for development but not for production.
- Create new folders to get the structure
src/app/shared/helper/mock. - Create or generate an interceptor service
MockBackendInterceptorwith the namemock-backend-interceptor.service.tsin this folder. - Move the existing helper files
angular-date-http-interceptor.ts,helper-service.ts,helper-service.spec.tsin the foldersrc/app/shared/helper. - Implement in the service
MockBackendInterceptor. Theinterceptmethod shall return anObservable<HttpEvent<any>>. - Create an observable with the of-operator and pipe the operators
mergeMap(handleRoute),materialize,delayanddematerialize. - Create a
handleRoute()function which checks the received url against the required urls and verbs. - Create a function for each required api (see list) and return the apropriate content and http code.
- Create a method
mockBackendProviderin themock-backend.tsfile - Create a
NgModulewith the nameMockModulein the filemock.module.ts. The Module is using themockBackendProviderin theprovider[]section. - Create in the folder
src/environmenta fileenvironment.model.ts. It shall contain theEnvironmentinterface (see the requirement underneath). - Change the 2 files in the folder
src/environmentand implement the interfaceEnvironment. Put the required information to the environment files (see the requirement underneath). - Place the
MockModulein the import section of theapp.module. Check ifuseMockBackendin the environment usesuseMockBackendtrue or false
The mock-server shall simulate the real backend in terms of several verbs per resource. The check of the correct authentication and authorisation will be implemented in one of the future tasks.
| verb | url | data |
|---|---|---|
| get | auctions | array of AUCTION_DATA |
| get | auctions/:id | AUCTION_DATA filtered to id |
| post | /users/authenticate | {username: xxx, password, yyy} |
| post | /users/register | user object |
| get | /users | array of user objects |
| get | /users/:id | user object |
| get | /users/:username | user object |
| delete | /users/:id | user object |
The mock server will be expanded during the project depending on the new api requirements.
Returned status codes
| status | function |
|---|---|
| 200 | ok |
| 204 | No Content |
| 401 | Unauthorized |
| 404 | Not found |
The user data shall be returned as a user object. The user object itself contains for now just a few attributes. To keep it simple for the moment, we do not have yet the user-role model.
{
id: 1,
username: 'felixmuster',
firstName: 'Felix',
lastName: 'Muster',
token: 'mock-jwt-token'
}
The command ng cli create at the beginning of this tutorial has generated in the folder src/enviroment two files: environment.ts and environment.prod.ts.
The files shall contain some variables to control the default and the prod environment. To get a consistent
model to all environments, we will introduce an interface Environment. Create a file environment.model.ts.
export interface Environment {
production: boolean;
useMockBackend: boolean;
version: string;
endpoints: {
backendBaseUrl: string;
backendAuthUrl: string;
};
}The default environment in the folder src/enviroment shall implement the interface Environment and shall
contain some useful information. The attribute useMockBackend: true is making sure the
MockBackend is used instead of the real backend.
import {Environment} from './environment.model';
export const environment: Environment = {
production: false,
useMockBackend: true,
version: '_VERSION_',
endpoints: {
backendBaseUrl: 'http://unused/api',
backendAuthUrl: 'http://unused/api'
}
};The prod environment in the folder src/enviroment shall implement the interface Environment and shall
contain some useful information. The attribute useMockBackend: false is making sure the
MockBackend is NOT used, We will use the real backend. The place holders '_VERSION_', '_BACKEND_BASE_URL_' and '_BACKEND_AUTH_URL_'
can be used to be replaced by a shell script before generating a docker container with environment variables.
import {Environment} from './environment.model';
// not for local serve/run!! (-> environment.prod-local.ts)
export const environment: Environment = {
production: true,
useMockBackend: false,
version: '_VERSION_',
endpoints: {
backendBaseUrl: '_BACKEND_BASE_URL_',
backendAuthUrl: '_BACKEND_AUTH_URL_'
}
}The persistence (simulation of the backend database) shall be implemented by the browsers local-store. It will allow us to save users for a longer time.
The use of the interceptor mock server will not show anymore the network traffic in the browser. For inspecting the calls it is useful to see the mockRequest and mockResponse.
Stop the test-auction-api server in the second console (use ctrl-c).
Compile and start the application with
$ ng serve
Open the project in the browser
- Open your browser with the address http://localhost:4200/
- Click in the menu to home and auctions
- You should see a page like the following illustration.
The home screen shall be displayed
Navigate to the auction list. The content shall be delivered by the mock server. One drawback of the mock server is... you will not see anymore the network traffic between frontend and backend in the browser developers tool (tab network). Instead, the mock server itself is creating request and response log content.

Navigate to the auction detail. The content shall be delivered by the mock server.
The new folder structure after creating the helper folder, the files in the mock folder and the model file in src/environments:
src
\- app
\- auction
\- auction-list
\- auction-list.component.css
\- auction-list.component.html
\- auction-list.component.ts
\- auction-list.component.spec.ts
\- auction-list-detail
\- auction-list-detail.component.css
\- auction-list-detail.component.html
\- auction-list-detail.component.ts
\- auction-list-detail.component.spec.ts
\- mouse-event-display
\- mouse-event-display.component.css
\- mouse-event-display.component.html
\- mouse-event-display.component.ts
\- mouse-event-display.component.spec.ts
\- shared
\- auction.ts
\- auction-data.service.spec.ts
\- auction-data.service.ts
\- auction-data.ts
\- auction.module.ts
\- auction-routing.module.ts
\- home
\- home.component.css
\- home.component.html
\- home.component.ts
\- home.component.spec.ts
\- nav-bar
\- nav-bar.component.css
\- nav-bar.component.html
\- nav-bar.component.ts
\- nav-bar.component.spec.ts
\-shared
\- helper
\- mock
\- mock.module.ts
\- mock-backend-interceptor.service.ts
\- mock-backend-interceptor.service.spec.ts
\- angular-date-http-interceptor.ts
\- helper.service.spec.ts
\- helper.service.ts
\- app.module.ts
\- app.component.html
\- app.component.spec.ts
\- app.component.ts
\- app.component.scss
\- app-routing.module.ts
\- assets
\- 01-yamaha-blue.png
\- 02-yamaha-aquamarine.png
\- 03-yamaha-red.png
\- environments
\- environment.prod.ts
\- environment.ts
\- environment.model.ts
\- _variables.scss
\- app.constansts.ts
\- favicon.ico
\- index.html
\- main.ts
\- polyfills.ts
\- styles.scss
\- test.ts
\- tsconfig.app.jsop
\- tsconfig.spec.json
\- typings.d.ts
The following code represents the mock backend server in the file mock-backend-interceptor.service.ts.
Registered users are saved in the localStore of the browser. The response has a programmed delay for
simulating the backend.
Pass Through Unmocked Requests to Real Backend
Sometimes there's a need to pass through specific requests to the server instead of being caught by the fake backend, for example when the real backend is partially completed and has some endpoints available.
With Angular 7/8/9/10 HTTP interceptors this is done by calling return next.handle(request);,
you can see the code for passing through requests at the bottom of the code sample below.
The code is copied and adapted from the web site:
https://jasonwatmore.com/post/2019/05/02/angular-7-mock-backend-example-for-backendless-development
- Create new folders to get the structure
src/app/shared/helper/mock. - Create or generate an interceptor service
MockBackendInterceptorwith the namemock-backend-interceptor.service.tsin this folder.
ng generate service shared/helper/mock/mock-backend-interceptor
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpResponse,
HttpHandler,
HttpEvent,
HttpInterceptor,
HTTP_INTERCEPTORS
} from '@angular/common/http';
import {Observable, of, Subscription, throwError} from 'rxjs';
import {delay, mergeMap, materialize, dematerialize, first, tap} from 'rxjs/operators';
import {AUCTION_DATA} from '../../../auction/shared/auction-data';
@Injectable()
export class MockBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const {url, method, headers, body} = request;
// wrap in delayed observable to simulate server api call
return of(null)
.pipe(mergeMap(() => handleRoute()))
.pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
.pipe(delay(500))
.pipe(dematerialize())
.pipe(tap({
next: data => {
console.log('mockResponse', data);
},
error: error => {
console.log('mockResponse', JSON.stringify(error));
},
complete: () => console.log('mockResponse: on complete')
}));
function handleRoute() {
console.log('mockRequest: ' + url, method, headers, body);
let response: Observable<HttpEvent<any>>;
switch (true) {
case url.endsWith('/users/authenticate') && method === 'POST':
response = authenticate();
break;
case url.endsWith('/users/register') && method === 'POST':
response = register();
break;
case url.endsWith('/users') && method === 'GET':
response = getUsers();
break;
case url.match(/(\/users[\/])/) && method === 'GET':
if (isNumber(nameFromUrl())) response = getUser();
else response = getUserByName();
break;
case url.match(/\/users\/\d+$/) && method === 'DELETE':
response = deleteUser();
break;
case url.match(/\/auctions\/\d+$/) && method === 'GET':
response = getAuction();
break;
case url.endsWith('/auctions') && method === 'GET':
response = getAuctions();
break;
default:
// pass through any requests not handled above
response = next.handle(request);
}
return response;
}
// route functions
function authenticate() {
const {username, password} = body;
let users = JSON.parse(localStorage.getItem('users')) || [];
const user = users.find(x => x.username === username && x.password === password);
if (!user) {
return error('Username or password is incorrect');
} else {
return ok({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
token: 'fake-jwt-token'
});
}
}
function register() {
const user = body;
let users = JSON.parse(localStorage.getItem('users')) || [];
console.log(users);
if (users.find(x => x.username === user.username)) {
return error('Username "' + user.username + '" is already taken')
}
console.log(user);
user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
console.log(user);
users.push(user);
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
function getUsers() {
let users = JSON.parse(localStorage.getItem('users')) || [];
if (!isLoggedIn()) return unauthorized();
return ok(users);
}
function getUser() {
let users = JSON.parse(localStorage.getItem('users')) || [];
users = users.filter(x => x.id === idFromUrl());
if (users.length > 0) {
return ok(users[0]);
} else {
return noContent('User with id ' + idFromUrl() + ' not found.')
}
}
function getUserByName() {
let users = JSON.parse(localStorage.getItem('users')) || [];
users = users.filter(x => x.username === nameFromUrl());
if (users.length > 0) {
return ok(users[0]);
} else {
return noContent('User with name ' + nameFromUrl() + ' not found.')
}
}
function getAuctions() {
return ok(AUCTION_DATA);
}
function getAuction() {
let auctions = AUCTION_DATA.filter(x => x.id === idFromUrl());
if (auctions.length > 0) {
return ok(auctions[0]);
} else {
return noContent('Auction item with id ' + idFromUrl() + ' not found.')
}
}
function deleteUser() {
if (!isLoggedIn()) return unauthorized();
let users = JSON.parse(localStorage.getItem('users')) || [];
users = users.filter(x => x.id !== idFromUrl());
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
// helper functions
function ok(body?) {
return of(new HttpResponse({status: 200, body}))
}
function error(message) {
return throwError({error: {message}});
}
function unauthorized() {
return throwError({status: 401, error: {message: 'Unauthorised'}});
}
function notFound() {
return throwError({status: 404, error: {message: 'Not found'}});
}
function noContent(message) {
return throwError({status: 204, error: {message: message}});
}
function isLoggedIn() {
return headers.get('Authorization') === 'Bearer fake-jwt-token';
}
function idFromUrl() {
const urlParts = url.split('/');
return parseInt(urlParts[urlParts.length - 1]);
}
function nameFromUrl() {
const urlParts = url.split('/');
return urlParts[urlParts.length - 1];
}
function isNumber(value: string | number): boolean
{
return ((value != null) && !isNaN(Number(value.toString())));
}
}
}
export const mockBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: MockBackendInterceptor,
multi: true
};The Mock Module is needed to load the mock server in terms of the chosen angular environment. The standard environment is using the mock server, the prod environment is not using it.
- Create or generate an angular module
MockModulewith the namemock.module.tsin the foldersrc/app/shared/helper/mock.
ng g module shared/helper/mock
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {mockBackendProvider} from './mock-backend-interceptor.service';
@NgModule({
declarations: [],
imports: [
CommonModule
],
providers: [mockBackendProvider]
})
export class MockModule {
}Make sure to add MockModule the in the file app.module.ts. The MockModule shall be used
only for environments with environment.useMockBackend: true.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AngularDateHttpInterceptorService } from './shared/helper/angular-date-http-interceptor.service';
import { NavBarComponent } from './nav-bar/nav-bar.component';
import { HomeComponent } from './home/home.component';
import { AppRoutingModule } from './app-routing.module';
import { environment } from '../environments/environment';
import { MockModule } from './shared/helper/mock/mock.module';
// import/use mock module only if configured (mock module will not be included in prod build!):
const mockModule = environment.useMockBackend ? [MockModule] : [];
@NgModule({
declarations: [AppComponent, NavBarComponent, HomeComponent],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
AppRoutingModule,
...mockModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AngularDateHttpInterceptorService,
multi: true
},
],
bootstrap: [AppComponent]
})
export class AppModule {}The unit test is not yet testing anything. We will introduce some real tests during the next task.
- use the file
/mock-backend-interceptor.service.spec.tsin the foldersrc/app/shared/helper/mock.
import { TestBed } from '@angular/core/testing';
import { MockBackendInterceptor } from './mock-backend-interceptor.service';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {RouterTestingModule} from '@angular/router/testing';
describe('MockBackendInterceptor', () => {
let service: MockBackendInterceptor;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule,
],
providers: [
MockBackendInterceptor
]
});
service = TestBed.get(MockBackendInterceptor);
});
it('should be created', () => {
const service: MockBackendInterceptor = TestBed.get(MockBackendInterceptor);
expect(service).toBeTruthy();
});
});Start the unit testing with:
ng test

The result show, some test are not working. We need to correct them:
- AppComponent
- AuctionDataService
- AuctionListComponent
- AuctionDetailComponent
- AuctionListDetailComponent
We need to fix the tests by importing the missing modules.
Jasmine 2.x was running in the defined order. From Jasmine 3.x test are executed randomly. The configuration in karma.config.js is correcting it:
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
jasmine: {
random: false,
}
},import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {AppRoutingModule} from './app-routing.module';
import {HttpClientModule} from '@angular/common/http';
import {HomeComponent} from './home/home.component';
import {NavBarComponent} from './nav-bar/nav-bar.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
AppRoutingModule,
HttpClientModule,
],
declarations: [
AppComponent,
HomeComponent,
NavBarComponent,
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'app works!'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app works!');
});
});import { TestBed } from '@angular/core/testing';
import { AuctionDataService } from './auction-data.service';
import {RouterTestingModule} from '@angular/router/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpClientTestingModule} from '@angular/common/http/testing';
describe('AuctionDataService', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule
],
providers: [AuctionDataService]
}));
it('should be created', () => {
const service: AuctionDataService = TestBed.get(AuctionDataService);
expect(service).toBeTruthy();
});
});import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AuctionListComponent } from './auction-list.component';
import {RouterTestingModule} from '@angular/router/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {AuctionListDetailComponent} from '../auction-list-detail/auction-list-detail.component';
import {AuctionDataService} from '../shared/auction-data.service';
import {HelperService} from '../../shared/helper/helper.service';
describe('AuctionListComponent', () => {
let component: AuctionListComponent;
let fixture: ComponentFixture<AuctionListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
],
declarations: [
AuctionListComponent,
AuctionListDetailComponent
],
providers: [
AuctionDataService,
HelperService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuctionListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AuctionDetailComponent } from './auction-detail.component';
import {RouterTestingModule} from '@angular/router/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {AuctionDataService} from '../shared/auction-data.service';
import {HelperService} from '../../shared/helper/helper.service';
describe('AuctionDetailComponent', () => {
let component: AuctionDetailComponent;
let fixture: ComponentFixture<AuctionDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule
],
declarations: [ AuctionDetailComponent ],
providers: [
AuctionDataService,
HelperService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuctionDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AuctionListDetailComponent } from './auction-list-detail.component';
import {RouterTestingModule} from '@angular/router/testing';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {AuctionDataService} from '../shared/auction-data.service';
import {HelperService} from '../../shared/helper/helper.service';
const dummyAuction =
{
id: 1,
seller: 'Felix',
category: {id: 1, name: 'Bikes', description: 'Motor bikes'},
auctionItem:
{
title: 'Yamaha YZF R1 Sports Bike',
description: 'Ready to take a "walk" on the wild side ',
picture: '01-yamaha-blue.png'
},
startDateTime: getDate(-5),
startingPrice: 4000,
fixedPrice: 5000,
bids:
[
{id: 1, amount: 4100, cancelExplanation: '', placedAtDateTime: getDate(-4), buyer: 'Max'},
{id: 2, amount: 4200, cancelExplanation: '', placedAtDateTime: getDate(-3), buyer: 'Klaus'},
{id: 3, amount: 4300, cancelExplanation: '', placedAtDateTime: getDate(-4), buyer: 'Nadja'}
],
endDateTime: getDate(1),
isFixedPrice: false,
minBidIncrement: 100
};
function getDate(days: number): Date {
return addDays(new Date(), days);
}
function addDays(date: Date, days: number): Date {
date.setDate(date.getDate() + days);
return date;
}
describe('AuctionListDetailComponent', () => {
let component: AuctionListDetailComponent;
let fixture: ComponentFixture<AuctionListDetailComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule
],
declarations: [ AuctionListDetailComponent ],
providers: [
AuctionDataService,
HelperService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AuctionListDetailComponent);
component = fixture.componentInstance;
component.auction = dummyAuction;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});