23

I keep getting the following error in my karma test even though my app is working perfectly with no errors. It is saying that there is no provider for Http. I'm using import { HttpModule } from '@angular/http'; in my app.module.ts file and adding it to the imports array. The karma error looks like the following:

Chrome 52.0.2743 (Mac OS X 10.12.0) App: TrackBudget should create the app FAILED
    Failed: Error in ./AppComponent class AppComponent_Host - inline template:0:0 caused by: No provider for Http!
    Error: No provider for Http!
        at NoProviderError.Error (native)
        at NoProviderError.BaseError [as constructor] (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/facade/errors.js:24:0 <- src/test.ts:2559:34)
        at NoProviderError.AbstractProviderError [as constructor] (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/di/reflective_errors.js:42:0 <- src/test.ts:15415:16)
        at new NoProviderError (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/di/reflective_errors.js:73:0 <- src/test.ts:15446:16)
        at ReflectiveInjector_._throwOrNull (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/di/reflective_injector.js:761:0 <- src/test.ts:26066:19)
        at ReflectiveInjector_._getByKeyDefault (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/di/reflective_injector.js:789:0 <- src/test.ts:26094:25)
        at ReflectiveInjector_._getByKey (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/di/reflective_injector.js:752:0 <- src/test.ts:26057:25)
        at ReflectiveInjector_.get (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/src/di/reflective_injector.js:561:0 <- src/test.ts:25866:21)
        at TestBed.get (webpack:/Users/ChrisGaona%201/budget-tracking/~/@angular/core/bundles/core-testing.umd.js:1115:0 <- src/test.ts:5626:67)
Chrome 52.0.2743 (Mac OS X 10.12.0): Executed 1 of 1 (1 FAILED) ERROR (0.229 secs / 0.174 secs)

Here is my app.component.ts file:

import {Component} from '@angular/core';
import {Budget} from "./budget";
import {BudgetService} from "./budget.service";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    providers: [BudgetService]
})
export class AppComponent {
    title = 'Budget Tracker';

    budgets: Budget[];
    selectedBudget: Budget;

    constructor(private budgetService: BudgetService) { }

    ngOnInit(): void {
        this.budgetService.getBudgets()
            .subscribe(data => {
                this.budgets = data;
                console.log(data);
                this.selectedBudget = data[0];
                console.log(data[0]);
            });
    }
}

Here is my simple spec:

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('App: TrackBudget', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
        declarations: [
            AppComponent
        ]
    });
  });

  it('should create the app', async(() => {
    let fixture = TestBed.createComponent(AppComponent);
    let app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

The error seems to be caused by my service, which can be seen here:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
import {Budget} from "./budget";

@Injectable()
export class BudgetService {

  constructor(public http: Http) { }

  getBudgets() {
    return this.http.get('budget.json')
        .map(response => <Budget[]>response.json().budgetData);

  }
}

If I remove the constructor(public http: Http) { } statement from the service, the test passes fine, but then the app fails in the browser. I have done quite a lot of research on this and have not been able to figure out the solution. Any help would be greatly appreciated!!

danday74
  • 52,471
  • 49
  • 232
  • 283
Chris
  • 233
  • 1
  • 4
  • 6

5 Answers5

24

The purpose of the TestBed is to configure an @NgModule from scratch for the testing environment. So currently all you have configured is the AppComponent, and nothing else (except the service that's already declared in the @Component.providers.

What I highly suggest you do though, instead of trying to configure everything like you would in a real environment, is to just mock the BudgetService. Trying to configure the Http and mock it is not the best idea, as you want want to keep external dependencies as light as possible when unit testing.

Here's what you need to do

  1. Create a mock for the BudgetService. I would check out this post. You can just extend that abstract class, adding your getBudgets method

  2. You need to override the @Component.providers, as mentioned in this post

If you really want to just use the real service and the Http, then you need to be prepared to mock connections on the MockBackend. You can't use the real backend, as it's dependent on the platform browser. For an example, check out this post. I personally don't think it's a good idea though when testing components. When testing your service, this is when you should do it.

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Thank you for your help! It seems to work, but I'm now getting this warning: `Critical dependency: the request of a dependency is an expression`. Is that normal or am I doing something wrong? – Chris Oct 13 '16 at 02:47
  • Not sure because i can't see what you did. It's not normal though – Paul Samsotha Oct 13 '16 at 02:48
  • Sorry about that. I really appreciate your help! Testing in Angular 2 is all kind of new to me right now. You can see the files I changed in the plunker I've made [here](https://plnkr.co/edit/wQGvov?p=catalogue). – Chris Oct 13 '16 at 03:13
  • Just quickly looking at it, you never set the data. Look at the example from the link in the first point. You will see an example at the bottom where the `content` is set on the mock. You also need to use `useValue` with an _instance_ of the mock, not `useClass` with the class of the mock. – Paul Samsotha Oct 13 '16 at 03:35
  • I don't think that will cause the error you are facing though. I just tested you code right now and it runs fine (no errors). – Paul Samsotha Oct 13 '16 at 03:45
  • Hmmm...that's interesting. I have added all of my main files in my src/app/ directory in the [plunker here](https://plnkr.co/edit/wQGvov?p=catalogue). I'm not sure if that will shed any light on my warning message. I generated the entire app with the angular 2 cli...I'm not sure if that has anything to do with it. This is the entire warning message: `WARNING in ./~/@angular/core/src/linker/system_js_ng_module_factory_loader.js 57:15 Critical dependency: the request of a dependency is an expression `. – Chris Oct 13 '16 at 04:40
  • That's a warning, not an error. It's an issue that was already raised on Github. It should be fixed in beta.17. I haven't confirmed the fix yet, I'm still on beta.16, too lazy to update. So yes I'm still seeing that dumb warning also. This shouldn't affect the tests though. – Paul Samsotha Oct 13 '16 at 04:42
  • I just want to point out that I was getting the same error and the first line of this answer made all the difference for me: "The purpose of the TestBed is to configure an @NgModule from scratch for the testing environment." I was importing HttpModule in my app, but not in my TestBed. Thank you so much for this answer! – Engineer_Andrew Apr 03 '17 at 22:16
15

Caution: This solution only works if you want to test the static structure. It won't work if your test actually makes service calls (and you better also have some of those tests).

Your test uses an own module definition, a testing module, and not your AppModule. So you have to import HttpModule there, too:

TestBed.configureTestingModule({
    imports: [
        HttpModule
    ],
    declarations: [
        AppComponent
    ]
});

You can also import your AppModule:

TestBed.configureTestingModule({
    imports: [
        AppModule
    ]
});

This has the advantage that you don't have to add new components and modules at many places. It's more convenient. On the other hand this is less flexible. You may be importing more than you'd wish in your test.

Furthermore you have a dependency from your low-level component to the whole AppModule. In fact that's kind of a circular dependency which is normally a bad idea. So in my eyes you should only do so for high-level components that are very central to your application anyway. For more low-level components which may be even reusable, you better list all dependencies explicitly in the test spec.

R2C2
  • 728
  • 6
  • 19
  • 2
    This won't work. You need to [use the mock backend](http://stackoverflow.com/a/39483673/2587435). – Paul Samsotha Oct 12 '16 at 17:01
  • 1
    @peeskillet You are right. This only works for tests that don't actually call the backend but merely test the static structure. Thanks for pointing this out. – R2C2 Oct 12 '16 at 17:46
6

On Angular 4+

RC2C's answer worked for me :) Thanks!

Caution: This will only work if you're not really calling your service. It works only if you want to test the static structure.

Just wanted to add that for Angular version 4 (and higher, probably) you should import HttpClientModule to your test bed, so that it looks like this:

import { HttpClientModule } from '@angular/common/http';


describe('BuildingService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
      providers: [BuildingService]
    });
  });

  it('should be created 2', inject([BuildingService], (service: BuildingService) => {
    expect(service).toBeTruthy();
  }));

}

Caution: See top Caution

Filip Savic
  • 2,737
  • 1
  • 29
  • 34
  • 1
    Other people who don't need *exactly* what the OP asked will come to this question (as I did) and read the responses. This might help some of them. – Filip Savic Feb 09 '18 at 15:20
3

Import HttpModule in app.module.ts and it will solve your problem.

import { HttpModule } from '@angular/http';

@NgModule({
    imports: [HttpModule]
})
...
SUBHASIS MONDAL
  • 705
  • 9
  • 20
  • Welcome to SO. Please read this [how-to-answer](http://stackoverflow.com/help/how-to-answer) for providing quality answer. – thewaywewere Apr 23 '17 at 11:44
  • It's a safe bet that any app making api calls through a service has that module installed already. – isherwood Jun 30 '17 at 21:20
0

An alternative to mocking the service as described in peeskillet's answer, is using the Mock Backend provided by angular.

The API doc contains the following example:

import {Injectable, ReflectiveInjector} from '@angular/core';
import {async, fakeAsync, tick} from '@angular/core/testing';
import {BaseRequestOptions, ConnectionBackend, Http, RequestOptions} from '@angular/http';
import {Response, ResponseOptions} from '@angular/http';
import {MockBackend, MockConnection} from '@angular/http/testing';

const HERO_ONE = 'HeroNrOne';
const HERO_TWO = 'WillBeAlwaysTheSecond';

@Injectable()
class HeroService {
  constructor(private http: Http) {}

  getHeroes(): Promise<String[]> {
    return this.http.get('myservices.de/api/heroes')
        .toPromise()
        .then(response => response.json().data)
        .catch(e => this.handleError(e));
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error);
    return Promise.reject(error.message || error);
  }
}

describe('MockBackend HeroService Example', () => {
  beforeEach(() => {
    this.injector = ReflectiveInjector.resolveAndCreate([
      {provide: ConnectionBackend, useClass: MockBackend},
      {provide: RequestOptions, useClass: BaseRequestOptions},
      Http,
      HeroService,
    ]);
    this.heroService = this.injector.get(HeroService);
    this.backend = this.injector.get(ConnectionBackend) as MockBackend;
    this.backend.connections.subscribe((connection: any) => this.lastConnection = connection);
  });

  it('getHeroes() should query current service url', () => {
    this.heroService.getHeroes();
    expect(this.lastConnection).toBeDefined('no http service connection at all?');
    expect(this.lastConnection.request.url).toMatch(/api\/heroes$/, 'url invalid');
  });

  it('getHeroes() should return some heroes', fakeAsync(() => {
       let result: String[];
       this.heroService.getHeroes().then((heroes: String[]) => result = heroes);
       this.lastConnection.mockRespond(new Response(new ResponseOptions({
         body: JSON.stringify({data: [HERO_ONE, HERO_TWO]}),
       })));
       tick();
       expect(result.length).toEqual(2, 'should contain given amount of heroes');
       expect(result[0]).toEqual(HERO_ONE, ' HERO_ONE should be the first hero');
       expect(result[1]).toEqual(HERO_TWO, ' HERO_TWO should be the second hero');
     }));

  it('getHeroes() while server is down', fakeAsync(() => {
       let result: String[];
       let catchedError: any;
       this.heroService.getHeroes()
           .then((heroes: String[]) => result = heroes)
           .catch((error: any) => catchedError = error);
       this.lastConnection.mockRespond(new Response(new ResponseOptions({
         status: 404,
         statusText: 'URL not Found',
       })));
       tick();
       expect(result).toBeUndefined();
       expect(catchedError).toBeDefined();
     }));
});
schnatterer
  • 7,525
  • 7
  • 61
  • 80
  • gotta love the copy and paste answers. – chris_r Feb 08 '18 at 18:31
  • @wordpress_designer This solution was what worked best for me. As I did not find it among the answers here, I shared it. If you want to improve it, use the edit button. If you deem it inappropriate, flag it. Stop trolling! – schnatterer Feb 09 '18 at 10:39