45

I have HeroMockService that will return mocked data and HeroService that will call back end service to retrieve heroes from database.

Assuming Angular2 has build environment, I'm planning to inject HeroMockService to AppComponent if current build environment is "dev-mock". If current build environment is "dev-rest", HeroService should be injected to AppComponent instead.

I would like to know how can I achieve this?

newbie
  • 553
  • 1
  • 5
  • 6
  • 1
    I would suggest renaming this to include the phrase "mock data" or "mock backend" -- its a really great question and great answer, it was hard to find! – Dmitri R117 Jul 24 '17 at 08:31

9 Answers9

31

See below my solution is based on @peeskillet one.

Create an interface that both the mock and real service implement, for your example IHeroService.

export interface IHeroService {
    getHeroes();
}

export class HeroService implements IHeroService {
    getHeroes() {
    //Implementation goes here
    }
}

export class HeroMockService implements IHeroService {
    getHeroes() {
    //Mock implementation goes here
}

Assuming you've created the Angular app using Angular-CLI, in your environment.ts file add the appropriate implementation, e.g.:

import { HeroService } from '../app/hero.service';

export const environment = {
  production: false,
  heroService: HeroService
};

For every different environment.(prod|whatever).ts you have to define a heroService pointing to the implementation and add the import.

Now, say you want to import the service in the AppModule class (you can do it on the component where you need the service as well)

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    FormsModule,
    HttpModule,
    AlertModule.forRoot()
  ],
  providers: [
    {
      provide: 'IHeroService',
      useClass: environment.heroService
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The important part is the provider:

  providers: [
    {
      provide: 'IHeroService',
      useClass: environment.heroService
    }

Now wherever you want to use the service you have to do the following:

import { IHeroService } from './../hero.service';

export class HeroComponent {

constructor(@Inject('IHeroService') private heroService: IHeroService) {}

Sources: Is it possible to inject interface with angular2? https://medium.com/beautiful-angular/angular-2-and-environment-variables-59c57ba643be http://tattoocoder.com/angular-cli-using-the-environment-option/

void
  • 1,454
  • 1
  • 14
  • 18
  • 2
    Sounds great but I get: `WARNING in Circular dependency detected: my.service.ts -> logger.service.ts -> src/environments/environment.ts -> my.service.ts` – Mick Dec 04 '17 at 15:33
  • @Mick I solved this by instead using a flag in `environment` to indicate which implementation, and `useFactory` in my `providers`. – nickwesselman Jan 03 '18 at 15:46
  • 4
    IMHO this should be the selected answer! It meets several clean code patterns like single responsibility pattern, the open closed principle and the inversion of control pattern more than every other solution. – deviolog Apr 13 '18 at 15:11
  • @techphoria414 I like the factory idea However this solution seems more clean because I have 20 services for each service i have to use factory provider which will be a repeat and also dev has to remember to use factory for every http service – Shailesh Vaishampayan Jun 17 '18 at 12:38
26

IMO, a better option would be to use the angular-in-memory-web-api.

note: this project was pulled into angular/angular from its old location.

It mocks the backend that Http uses, so instead of making an actual XHR call, it just grabs the data that you provide to it. To get it, just install

npm install --save angular-in-memory-web-api

To create the database you implement the createDb method in your InMemoryDbService

import { InMemoryDbService } from 'angular-in-memory-web-api'

export class MockData implements InMemoryDbService {
  let cats = [
    { id: 1, name: 'Fluffy' },
    { id: 2, name: 'Snowball' },
    { id: 3, name: 'Heithcliff' },
  ];
  let dogs = [
    { id: 1, name: 'Clifford' },
    { id: 2, name: 'Beethoven' },
    { id: 3, name: 'Scooby' },
  ];
  return { cats, dogs, birds };
}

Then configure it

import { InMemoryWebApiModule } from 'angular-in-memory-web-api';

@NgModule({
  imports: [
    HttpModule,
    InMemoryWebApiModule.forRoot(MockData, {
      passThruUnknownUrl: true
    }),
  ]
})

Now when you use Http and make a request to /api/cats it will get all the cats from the db. If you go to /api/cats/1 it will get the first cat. You can do all the CRUD operations, GET, POST, PUT, DELETE.

One thing to note is that it expects a base path. In the example /api is the base path. You can also configure a root (this is different from base) path, in the configuration

InMemoryWebApiModule.forRoot(MockData, {
  rootPath: 'root',
  passThruUnknownUrl: true // forwards request not in the db
})

Now you can use /root/api/cats.


UPDATE

In regards to the question about how to switch from dev to production, you can use a factory to create the providers. Same would be true if you were to use your mock service instead of the in-memory-web-api

providers: [
  Any,
  Dependencies
  {
    // Just inject `HeroService` everywhere, and depending
    // on the environment, the correct on will be chosen
    provide: HeroService, 
    useFactory: (any: Any, dependencies: Dependencies) => {
      if (environment.production) {
        return new HeroService(any, dependencies);
      } else {
        return new MockHeroService(any, dependencies);
      }
    },
    deps: [ Any, Dependencies ]
]

As far as the in-memory-web-api, I need to get back to you (I need to test a theory). I just started using it, and haven't gotten to the point where I need to switch to production. Right now I just have the above configuration. But I'm sure there's a way to make it work without having to change anything

UPDATE 2

Ok so for the im-memory-web-api what we can do instead of importing the Module, is to just provide the XHRBackend that the module provides. The XHRBackend is the service that Http uses to make XHR calls. The in-memory-wep-api mocks that service. That's all the module does. So we can just provide the service ourselves, using a factory

@NgModule({
  imports: [ HttpModule ],
  providers: [
    {
      provide: XHRBackend,
      useFactory: (injector: Injector, browser: BrowserXhr,
                   xsrf: XSRFStrategy, options: ResponseOptions): any => {
        if (environment.production) {
          return new XHRBackend(browser, options, xsrf);
        } else {
          return new InMemoryBackendService(injector, new MockData(), {
            // This is the configuration options
          });
        }
      },
      deps: [ Injector, BrowserXhr, XSRFStrategy, ResponseOptions ]
    }
  ]
})
export class AppHttpModule {
}

Notice the BrowserXhr, XSRFStrategy, and ResponseOptions dependencies. This is how the original XHRBackend is created. Now instead of importing the HttpModule into your app module, just import the AppHttpModule.

As far as the environment, that's something you need to figure out. With angular-cli, the is already an environment that gets automatically switched to production when we build in production mode.

Here's the complete example I used to test with

import { NgModule, Injector } from '@angular/core';
import { HttpModule, XHRBackend, BrowserXhr,
         ResponseOptions,  XSRFStrategy } from '@angular/http';

import { InMemoryBackendService, InMemoryDbService } from 'angular-in-memory-web-api';

let environment = {
  production: true
};

export class MockData implements InMemoryDbService {
  createDb() {
    let cats = [
      { id: 1, name: 'Fluffy' }
    ];
    return { cats };
  }
}

@NgModule({
  imports: [ HttpModule ],
  providers: [
    {
      provide: XHRBackend,
      useFactory: (injector: Injector, browser: BrowserXhr,
                   xsrf: XSRFStrategy, options: ResponseOptions): any => {
        if (environment.production) {
          return new XHRBackend(browser, options, xsrf);
        } else {
          return new InMemoryBackendService(injector, new MockData(), {});
        }
      },
      deps: [ Injector, BrowserXhr, XSRFStrategy, ResponseOptions ]
    }
  ]
})
export class AppHttpModule {
}
Josh
  • 3,035
  • 2
  • 23
  • 40
Paul Samsotha
  • 188,774
  • 31
  • 430
  • 651
  • 1
    Assuming I switched to `angular-in-memory-web-api`, referring to my question, how to switch from _mock_ mode into _real_ request mode without major changes (like modifying all `import` statements)? I'm aiming at something as simple as changing build environment (like from dev mode to prod mode). – newbie Oct 09 '16 at 10:24
  • See my update for the mock service. I will need to get back to you on using he in-memory-web-api. Something I would need to test out. Don't have my tools right now – Paul Samsotha Oct 09 '16 at 10:48
  • 1
    Where does `environment` come from? – Saad Farooq Jan 08 '17 at 00:52
  • Environments can be configured in `.angular-cli.json` file, by default cli generates two environments: `dev` and `prod`. In the original question there is a third, custom environment (`dev-mock`). You can switch between environments using -e, for example: `"ng serve -e dev-mock"` – István Békési Jun 14 '17 at 12:39
  • This a great approach, but I am not following new XHRBackend(browser, options, xsrf); What is the browser argument used for? – Winnemucca Aug 25 '17 at 21:48
10

A lot of these answers are correct but will fail tree shaking. The mock services will be included as part of the final application which is usually not what is desired. In addition, it isn't easy to switch in and out of mock mode.

I have created a working example which solves these problems: https://github.com/westonpace/angular-example-mock-services

  1. For each service, create an abstract class or a value token & interface. Create a mock service and a real service which implement the abstract class / interface.
  2. Create a MockModule which provides all of your mock services and a RealModule which provides all of your real services (make sure to use the useClass/provide fields to provide the interface/abstract class)
  3. In the appropriate environment.*.ts files load either the RealModule or the MockModule
  4. Make changes to the angular.json file used by angular-cli to create a new build target mock which builds with the mock environment file which injects the MockModule. Create a new serve configuration which serves up the mock build so you can do ng serve -c mock. Change the default protractor configuration so that it uses the mocked serve target so ng e2e will run against your mock services.
Pace
  • 33,215
  • 10
  • 99
  • 130
  • Does this approach require anything special to be done with the unit tests (beyond what we might normally do)? – theMayer Dec 11 '18 at 15:35
  • 1
    @theMayer No. If your mock modules are in your app module then unit tests shouldn't be loading the entire app module. If your mock modules are in library projects then your unit test can load `LibraryCommonModule` and `LibraryMockModule` directly (unless you actually want to run the production module in your unit test in which case you can load that directly). – Pace Dec 11 '18 at 23:52
  • I followed this and it seems to work very well. Thanks – theMayer Dec 13 '18 at 18:28
  • 2
    By far the best approach, with a full example on top of that. It should be the accepted answer IMO – Carrm Mar 17 '19 at 07:35
  • The problem with this solution is that if your real services also reference the environment file (for example to read an API URI), you end up with circular references from the service, to the environment, to the module, and back to the service. – Jon Rimmer Oct 04 '19 at 10:10
  • @JonRimmer That is correct. Could you work around it by injecting the API URI with a value provider? – Pace Oct 04 '19 at 16:55
  • Not a critique, but curious. Does the answer provided by @void allow tree-shaking? You're defining all your real/mock services in Modules, and void is doing so in environment files. Any important differences between the two methods? – jmrah Sep 20 '20 at 23:58
  • 1
    @jrahhali void's method should allow for tree-shaking. It should work fine as long as your mock services don't rely on external modules you won't end up using in production. I think at the time I wrote the answer I had a self-contained test-only `SimulatorModule` that I used in a couple of different places. – Pace Sep 21 '20 at 19:17
7

If you organize your code into Angular2 modules, you can create an additional module to import mocked services, for example:

@NgModule({
   providers: [
      { provide: HeroService, useClass: HeroMockService }
   ]
})
export class MockModule {}

Assuming that you declare import for normal services in your core module:

@NgModule({
   providers: [ HeroService ]
})
export class CoreModule {}

As long as you import MockModule after CoreModule, then the value that will be injected for HeroService token is HeroMockService. This is because Angular will use the latest value if there are two providers for the same token.

You can then customize import for MockModule based on certain value (that represents build environment), for example:

// Normal imported modules
var importedModules: Array<any> = [
   CoreModule,
   BrowserModule,
   AppRoutingModule
];

if (process.env.ENV === 'mock') {
   console.log('Enabling mocked services.');
   importedModules.push(MockModule);
}

@NgModule({
    imports: importedModules
})
export class AppModule {}
jocki
  • 1,688
  • 16
  • 26
1

A great and simple solution is provided on this post by Luuk G.

https://hackernoon.com/conditional-module-imports-in-angular-518294aa4cc

In summary:

let dev = [
StoreDevtoolsModule.instrument({
    maxAge: 10,
  }),
];

// if production clear dev imports and set to prod mode
if (process.env.NODE_ENV === 'production') {
  dev = [];
  enableProdMode();
}

@NgModule({
  bootstrap: [
    Base,
  ],
  declarations: [
    Base,
  ],
  imports: [
    Home,
    RouterModule.forRoot(routes, { useHash: true }),
    StoreModule.forRoot(reducers.reducerToken),
    ...dev,
  ],
  providers: [
  ],
})
export class Root { }
Dmitri R117
  • 1,412
  • 15
  • 13
1

This worked for me in Angular 6. Leverages @void's Interface approach (where both the true service and the mock service implement the service interface). The accepted answer neglects to mention that the purpose of a factory is to return an implementation of an interface.

heroservice/heroservice.module.ts

import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IHeroService } from './ihero.service';
import { HeroService } from './hero.service';
import { MockHeroService } from './mock-hero.service';


@NgModule({
  imports: [
    CommonModule
  ],
})
export class HeroServiceModule {
  static forRoot(mock: boolean): ModuleWithProviders {
    return {
      ngModule: HeroServiceModule ,
      providers: [
        {
          provide: IHeroService,
          useClass: mock ? MockHeroService : HeroService,
        },
      ],
    };
  }
}

app.module.ts

import { NgModule, isDevMode } from '@angular/core';
import { HeroServiceModule } from './heroservice/heroservice.module';

// ...

  imports: [
    BrowserModule,
    HttpClientModule,
    // ...
    HeroServiceModule.forRoot(
      isDevMode() // use mock service in dev mode
    ),
  ],

// ...

some/some.component.ts

Import and use the interface as you would the actual service (the implementation will be "provided" at runtime).

import { IHeroService } from '../heroservice/iheroservice.service';

// ...
FizxMike
  • 872
  • 9
  • 16
0

For those like me getting the following error:

ERROR in Error encountered resolving symbol values statically.
Function calls are not supported. Consider replacing the function
or lambda with a reference to an exported function

For my purpose it was for an ErrorHandler:

{
    provide: ErrorHandler,
    useFactory: getErrorHandler,
    deps: [ Injector ]
}
...
export function getErrorHandler(injector: Injector): ErrorHandler {
    if (environment.production) {
        return new MyErrorHandler(injector);
    } else {
        return new ErrorHandler();
    }
}
RVandersteen
  • 1,831
  • 1
  • 18
  • 42
0

Here's an example of the latest way to do it with conditional environment logic. Note that EventService and LogService are just examples of other services you may have).

providers: [
    EventService,
    LogService,
    MyService,
    {
      provide: MyService,
      useClass: (environment.custom == null || environment.custom === '' || environment.custom === 'MyCustomerService')
        ? MyCustomService : MyService
    }
  ]
]
Chris K
  • 24
  • 3
-1

@Inject('IHeroService') does't go well with production. In my case I just did this:

import { Injectable } from '@angular/core';
import * as Priority from 'priority-web-sdk';


@Injectable()
export class PriorityService
{
    priority;
    constructor( ){
        this.priority=Priority;
    }
}

The Priority library that I wanted to import did not have an exported module so I needed to cheat a little. At first I tried the @Inject('IHeroService') way but when I compiled as prod it didn't work.

Hope it helps someone!

neomib
  • 1,809
  • 2
  • 11
  • 22