Angular 2 service providers: Flexibly replacing a concrete or 'real' Angular 2 service module

This post presents a pattern for flexibly replacing a concrete or 'real' Angular 2 service module (that talks to a Web API) with a *mock* service module.

The goal is to have an Angular 2 app that is testable WITHOUT having to connect to a real Web API.

This can be useful for dev-testing, and for unit tests.
I've used the pattern with Angular 2 RC4 and for 2.1 .. 2.4

/////////////////////////////////////////
//app.component.ts
//
//an app that uses dependency injection to automatically inject a REAL or a MOCK ng2 'car' service provider, according to the running environment

import { Component, OnInit } from '@angular/core';

import { BaseProxyCarService, ProxyCarProviderFactory } from './car-service-proxy/index';

@Component({
    moduleId: module.id,
    selector: 'app-car-list',
    providers: [ProxyCarProviderFactory.getActiveProvider({
      // OPTIONAL: pass a param here to FORCE using the REAL web service on the main site.
      // normally this should be false.
      isUsingLocalMainSiteForWebServices: false
    })],
    templateUrl: './app.component.html',
    })
// Component class
export class CarListComponent implements OnInit {
    // Constructor with injected service
    constructor( private _carService: BaseProxyCarService ) {
    }

    // Get all car data
    loadCars() {
      this._carService.getRecentCars()
                         .subscribe(
                            cars => {
                                //do something with the cars ...
                            },
                            err => console.log(err)
                         );
    }

    ngOnInit() {
    this.loadProjects();
  }
}

/////////////////////////////////////////
//proxy-myservice-provider.factory.ts

//ProxyCarService is a standard Angular 2 service module that knows how to talk to a 'Car' Web API that provides Car data.
//MockProxyCarService is a mock version of that service, that does NOT talk to the real Web API
//Both ProxyCarService and MockProxyCarService derive from an abstract class BaseProxyCarService, so that they can be injected via the provider (ProxyCarProviderFactory).

import { BaseProxyCarService } from './base-proxy-car.service';
import { MockProxyCarService } from './mock-proxy-car.service';
import { ProxyCarService } from './proxy-car.service';

export class EnvironmentUtilsSettings {
    /**
    // set to true, for debugging against REAL web services on main site
    // (requires turning OFF CORS prevention in Chrome):
     */
    isUsingLocalMainSiteForWebServices: boolean = false;
}

// The provider, which provides the correct derivation of BaseProxyCarService according to the environment.
// note: both MockProxyCarService and ProxyCarService would derive from the abstract class BaseProxyCarService
export class ProxyProjectProviderFactory {
    public static getActiveProvider(settings: EnvironmentUtilsSettings): any {
        let provider = { provide: BaseProxyCarService, useClass: ProxyCarService };
        let mockProvider = { provide: BaseProxyCarService, useClass: MockProxyCarService };

        // we are running in a dev server outside of the main site, so we cannot use real web api due to Cors
        // so to enable dev testing, we use a mock service:
        let activeProvider = ProxyProjectProviderFactory.isInDevServer(settings) ? mockProvider : provider;
        return activeProvider;
    }

    //Determines whether we are running on a site that DOES have web services.
    //Alternative logic could be to check are we running under unit test (under karma for example)
    public static isInDevServer(settings: EnvironmentUtilsSettings): boolean
    {
        let origin = window.location.origin;

        //here the local main site when developing WITH web services is at port 5000
        //if we are on localhost but NOT at port 5000 then we are only running a smaller site WITHOUT the web services
        let isSiteUrlDevServer = origin.indexOf('localhost:') >= 0
            && origin.indexOf('localhost:5000') < 0;

        if (settings.isUsingLocalMainSiteForWebServices) {
            return false;
        }

        return isSiteUrlDevServer;
    }
}

Comments