Skip to content

Instantly share code, notes, and snippets.

@florestankorp
Last active May 2, 2023 11:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save florestankorp/6512031dbcfb400142394b696975789c to your computer and use it in GitHub Desktop.
Save florestankorp/6512031dbcfb400142394b696975789c to your computer and use it in GitHub Desktop.
Scenario based API mocking with `ng-apimock`

Scenario based API mocking with ng-apimock

Overview

ng-apimock is a tool that provides scenario based API mocking. It can be used to test and develop applications without the need of a backend. Alternatively you can also chose to let certain calls through to the actual backend and apply mocking selectively.

Core concepts

  • Most files used for implementing ng-apimock in this workspace are located in the ./ng-apimock folder. This makes it easy to locate and change files.
  • Configuration and further functionality for ng-apimock can be found in ./ng-apimock/start-ng-apimock.ts
  • Mock files are located in ./ng-apimock/mocks, this will be picked up by the mock server when started.
  • Inside of your app you will define environment specific defaults and the extend project.json, to create custom build and serve commands for a workflow that uses mocking
  • Scripts inside of package.json tie the steps together to give you a runnable command, which starts your app and the mock server

Note: Jump ahead to Starting the mock server if you just want to start and run your app against a mock server.

How it works

To make make fetch calls in your Angular app and use mocked values, use the environment variables of your app. This way you can inject the right values required for each environment. For use with the mocking server you can follow these steps.

Create an environment.mock.ts file

export const environment: Environment = {
  production: false,
  /**
   * This is the URL that will be detected by the proxy.
   * It can be anything as long as it is unique, but it
   * is a good idea to use '/ng-apimock' to make clear
   * where calls are coming from :)
   */
  myEndpoint: 'my-endpoint/ng-apimock',
};

Make fetch calls as follows:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import type { Observable } from 'rxjs';
import { environment } from '../environments/environment';
import { MyResponseType } from './types';

@Injectable({
  providedIn: 'root',
})
export class MyService {
  public constructor(private readonly http: HttpClient) {}

  public fetchData(): Observable<MyResponseType[]> {
    // Note: use of 'environment' here
    return this.http.get<MyResponseType[]>(environment.myEndpoint);
  }
}

To use the environment variable when starting your app, add it to your project.json

{
  "targets": {
    "build": {
      "configuration": {
        ...
        "mock": {
          "fileReplacements": [
            {
              "replace": "apps/projects/<your-project>/<your-app>/src/environments/environment.ts",
              "with": "apps/projects/<your-project>/<your-app>/src/environments/environment.mock.ts"
            }
          ]
        }
      },
      "serve": {
        ...
        // This setting will be used to run your app using the build command 'build:mock'
        "mock": {
          "browserTarget": "projects-<your-project>-<your-app>:build:mock",
          // Note: use of proxy here. Reference below...
          "proxyConfig": "ng-apimock/proxy.conf.json"
        }
      }
    }
  }
}

Register your mock endpoint in ng-apimock/proxy.conf.json. Calls to this endpoint will now be proxied to the port where the ng-apimock server is running.

{
    "/my-endpoint/ng-apimock": {
      "target": "http://localhost:9000",
      "secure": false,
      "logLevel": "debug"
    },  
    "/ng-apimock/*": {
      "target": "http://localhost:9000",
      "secure": false,
      "logLevel": "debug"
    }
  }

In order to start the mock server you will need a startup script:

ng-apimock/start-ng-apimock.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
const apimock = require('@ng-apimock/core');
const { createProxyMiddleware } = require('http-proxy-middleware');
const devInterface = require('@ng-apimock/dev-interface');
const express = require('express');
const app = express();
app.set('port', 9000);

apimock.processor.process({
  src: 'ng-apimock/mocks',
  patterns: {
    mocks: '**/*mock.js',
    presets: '**/*preset.js',
  },
});

app.use(apimock.middleware);
app.use('/dev-interface', express.static(devInterface));

/**
 * Add your proxy middleware here:
 */
app.use(
  '/my-endpoint/ng-apimock',
  createProxyMiddleware({
    target: '<my-real-endpoint>',
    pathRewrite: {
      '^/my-endpoint/ng-apimock': '',
    },
    changeOrigin: true,
    secure: false,
  })
);

app.listen(app.get('port'), () => {
  const port = app.get('port');
  console.log(`@ng-apimock/core running on http://localhost:${port}`);
  console.log(
    `@ng-apimock/dev-interface is available under http://localhost:${port}/dev-interface`
  );
});

To use the passThrough scenario of ng-apimock, i.e. to bypass mocking and call the backend directly, you can add your endpoint to ng-apimock/start-ng-apimock.ts where you will create a createProxyMiddleware

/**
 * For calls you wish to pass through to your backend:
 * Add your proxy middleware here
 */
app.use(
  '/my-endpoint/ng-apimock',
  createProxyMiddleware({
    target: '<my-real-endpoint>',
    pathRewrite: {
      '^/my-endpoint/ng-apimock': '',
    },
    changeOrigin: true,
    secure: false,
  })
);

Note: You will need a service running on the target, in order for this to work!

TypeScript

We can write typesafe mocks that adhere to the ng-apimock schema and give warnings when incorrect properties are provided.

Here is the TypeScript interface for the ng-apimock schema:

export interface NgAPIMockSchema<T> {
  name: string;
  isArray?: boolean;
  delay?: number;
  request: MockRequest;
  responses: Record<string, MockResponse<T>>;
}

export interface MockRequest {
  url: string;
  method: 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'POST' | 'PUT';
  body?: Record<string, unknown>;
  headers?: Record<string, unknown>;
}

export interface MockResponse<T> {
  data: T;
  default: boolean;
  file?: string;
  headers?: Record<string, string>;
  status?: number;
  statusText?: string;
  then?: MockResponseThenClause;
}
export interface MockResponseThenClause {
  criteria?: MockResponseThenClauseCriteria;
  mocks: [
    MockResponseThenClauseMockSelection,
    ...MockResponseThenClauseMockSelection[]
  ];
}
export interface MockResponseThenClauseCriteria {
  times: number;
}
export interface MockResponseThenClauseMockSelection {
  name?: string;
  scenario: string;
}

Note: NgAPIMockSchema accepts a generic type that lets you pass the desired type of your API response and assigns it to data

Linting

If your ng-apimock folder doesn't fall under the ESLint config of your project, make sure to add it and to extend the root .eslintrc.json:

ng-apimock/.eslintrc.json

{
  "extends": [
    "../.eslintrc.json"
  ],
  "ignorePatterns": [
    "!**/*",
    "start-ng-apimock.ts"
  ],
  "overrides": [
    {
      "files": [
        "*.ts"
      ],
      "rules": {},
      "extends": []
    }
  ]
}

Writing mocks

As mentioned above, mocks are written in TypeScript and can be found in ng-apimock/mocks. Mock files contain scenarios for a single endpoint and a single HTTP method, which means a *.mock.ts file can be viewed as a collection of scenarios.

For topics such as advanced request matching​, chaining responses​ or returning file data​ please read the ng-apimock documentation.

For now, here are a few conventions that will help us keep things neat and organized:

  1. Please make a folder for the service you are mocking.
  2. Please use this naming convention: <endpoint>-<HTTP method>.mock.ts, e.g. my-endpoint-GET.mock.ts
  3. Consider keeping the name of the mocks module.export the same, and let it reflect the service, endpoint and method it applies to. So for example myAPIGETmyEndpoint
  4. It's advised to use part of the HTTP messages as a starting point in your naming, so consider names like ok, notFound or internalServerError

Example mock where data will be of type MyResponseType[]. It has the following scenarios:

  • ok
  • created
  • internalServerError

ng-apimock/mocks/my-api/my-endpoint-GET.mock.ts

import { NgAPIMockSchema } from '../ng-apimock.model';
import { MyResponseType } from './types';

const myAPIGETmyEndpoint: NgAPIMockSchema<MyResponseType[]> = {
  name: 'myAPIGETmyEndpoint',
  request: {
    url: '/my-endpoint',
    method: 'GET',
  },
  responses: {
    ok: {
      default: true,
      status: 200,
      data: []// mock data for the scenario 'ok' goes here
    },
    created: {
      default: false,
      status: 201,
      data: []// mock data for the scenario 'created' goes here
    },
    internalServerError: {
      default: false,
      status: 500,
      data: null // can also be 'null'
    },
  },
};

module.exports = myAPIGETmyEndpoint;

Starting the mock server

Before getting started, make sure to install the necessary dependencies by running: npm install

package.json

  "scripts": {
    "nx": "npx nx",
    "start:mock": "ts-node ./ng-apimock/start-ng-apimock.ts",
    "serve:<my-app>:mock": "nx serve projects-<my-project>-<my-app> --configuration=mock",
    "start-mock:serve:<my-app>:mock": "concurrently --kill-others \"npm run start:mock\" \"npm run serve:<my-app>:mock\""
  },

start:mock

Starts the mock server with the mocks created in the previous step

serve:my-app:mock

Starts your app with the configuration mock

start-mock:serve:<my-app>:mock

Combines the above steps into a single command

Note: Make sure to replace <my-project>-<my-app> with the values that apply to your own project and app! Check in the workspace level package.json which scripts are already available and run those. Chances are your app already has these commands.

After executing npm run start-mock:serve:<my-app>:mock command, you should see the following output in your terminal:

[HPM] Proxy created: /  -> my-real-endpoint
[0] [HPM] Proxy rewrite rule created: "^/my-endpoint/ng-apimock" ~> ""
[0] @ng-apimock/core running on http://localhost:9000
[0] @ng-apimock/dev-interface is available under http://localhost:9000/dev-interface
[1] ** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

This confirms that both the app and the mock server were started successfully and the proxies were configured correctly.

From here you can go to http://localhost:9000/dev-interface and see an overview of your mocked endpoints and can select scenarios, add delay and much more. Read more about the dev-interface here.

Dependencies

  • @ng-apimock/core
  • @ng-apimock/dev-interface
  • concurrently
  • http-proxy-middleware
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment