简体   繁体   中英

Unit testing and mocking a service with DI

I have been struggling with this for a while, and I'm hoping someone can help. I have a component that uses a service to get data. I am attempting to add unit tests to it. My problem is that the tests always fail with "Error: No provider for Http". Here is my code:

Service:

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';

import { Contact } from './contact.model';

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

    public getContacts(): Observable<Array<Contact>> {
        return this.http.get('assets/contacts.json').map(res => {
            let r = res.json().Contacts;
            return r;
        });
    }
}

Component:

import { Component, OnInit, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Contact } from '../contact.model'; 
import { ContactsService } from '../contacts.service';

@Component({
    selector: 'app-contacts',
    templateUrl: './contacts.component.html',
    styleUrls: ['./contacts.component.css'],
    providers: [ContactsService]
})
export class ContactsComponent implements OnInit {

    contactsAll: Array<Contact>;
    contacts: Array<Contact>;

    constructor(private contactsService: ContactsService) { }

    ngOnInit() {
        this.contactsService.getContacts().subscribe((x) => {
            this.contactsAll = x;
            this.contacts = this.contactsAll;
        });
    }

}

Tests:

import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Rx';

import { ContactsComponent } from './contacts.component';
import { ContactsService } from '../contacts.service';
import { Contact } from '../contact.model';

class MockContactsService extends ContactsService {

    constructor() {
        super(null);
    }

    testContacts: Array<Contact> = [
        new Contact("test1 mock", 12345, 10000),
        new Contact("test2 mock", 23456, 20000)
    ];

    public getContacts(): Observable<Array<Contact>> {
        return Observable.of(this.testContacts);
    }
}

describe('ContactsComponent', () => {
    let component: ContactsComponent;
    let fixture: ComponentFixture<ContactsComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [FormsModule],
            declarations: [ContactsComponent],
           // providers: [{ provide: ContactsService, useClass: MockContactsService }] // this is needed for the service mock
        }).overrideComponent(ContactsService, {// The following is to override the provider in the @Component(...) metadata
            set: {
                providers: [
                    { provide: ContactsService, useClass: MockContactsService },
                ]
            }
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(ContactsComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });

    describe('1st test', () => {
        it('true is true', () => expect(true).toBe(true));
    })
});

Let's try this:

First, move your providers array from your component to your NgModule. It's better to provide your services at the module level since it de-couples your providers from your component tree structure (unless you specifically want to have a separate instance of a provider per component, and from your simplified use case, there's no need for a separate instance per component).

so,

@Component({
    selector: 'app-contacts',
    templateUrl: './contacts.component.html',
    styleUrls: ['./contacts.component.css'],
   /// providers: [ContactsService]  <-- remove this line
})
export class ContactsComponent implements OnInit {
   .....

and add it to the NgModule that declares your ContactsComponent

 @NgModule({
    imports: ..
    declarations: ...
    providers: [ContactsService] // <-- provider definition moved to here
 })
 export class ModuleDeclaringContactsComponent

Once you do that, then mocking the ContactsService in your test is easy to implement.

TestBed.configureTestingModule({
        imports: [FormsModule],
        declarations: [ContactsComponent],
        providers: [{ provide: ContactsService, useClass: MockContactsService }] // this is needed for the service mock
    });

With that, you should be good to go.

Sorry everyone - turns out it was something completely different.

I modified my code as per snorkpete's answer, and I am going to mark that as the answer, as I believe that is the cleanest approach.

The real problem came from using Angular Cli to create my project. It automatically created tests for my component and my service. This meant the code in the service test was causing the error, not the code in the component. I commented out the code in the service test and everything passed.

Annoyingly, there was no indication in any of the failures that this is where the error was coming from!

In case if component should have Service in providers we can ovverride metadata by TestBed.overrideComponent() :

@Component({
    selector: 'app-contacts',
    templateUrl: './contacts.component.html',
    styleUrls: ['./contacts.component.css'],
    providers: [ContactsService] // IF YOU NEED IT
})

we need to do next:

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [ SomeComponent ],
        providers: [
            {provide: SomeService, useValue: SomeMock},
            provideMockStore({system: {userConfig: {viewSettings: {theme: ThemeName.light}}}} as any)
        ],
        schemas: [NO_ERRORS_SCHEMA]
    })
    .compileComponents();
    TestBed.overrideComponent(SomeComponent , { set: { providers: [{provide: SomeService, useValue: SomeMock}]}})
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SomeComponent );
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

more info here https://codecraft.tv/courses/angular/unit-testing/dependency-injection/

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM