Search code examples
angularangular2-testing

Test Angular 2.0.0 component with HTTP request


I have the following Angular 2.0.0 component:

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

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.css']
})
export class BookListComponent implements OnInit {
  books: any;

  constructor(private http: Http) { }

  ngOnInit() {
    this.http.get('/api/books.json')
      .subscribe(response => this.books = response.json());
  }

}

How would I test the ngOnInit() function?

I don't want to include the test I've tried so far because I suspect I'm way off the right track and I don't want to bias the answers.


Solution

  • To mock Http, you need to configure the MockBackend with the Http provider in the TestBed. Then you can subscribe to its connections an provide mock responses

    beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            {
              provide: Http, useFactory: (backend, options) => {
                return new Http(backend, options);
              },
              deps: [MockBackend, BaseRequestOptions]
            },
            MockBackend,
            BaseRequestOptions
          ]
        });
    });
    

    How would I test the ngOnInit() function?

    The problem is the asynchronous nature of the Http.get call. ngOnInit will be called when you call fixture.detectChanges(), but the the asynchronous nature of Http causes the test to run before the Http is finished. For that we an use fakeAsync, as mentioned here, but the next problem is that you can't use fakeAsync and templateUrl. You can hack it with a setTimeout and test in there. That would work. I personallu don't like it though.

    it('', async(() => {
      setTimeout(() => {
         // expectations here
      }, 100);
    })
    

    Personally, I think you have a design flaw to begin with. The Http calls should be abstracted into a service, and the component should interact with the service, not directly with the Http. If you change your design to this (which I would recommend), then you can test the service like in this example, and for the component testing, create a synchronous mock, as mentioned in this post.

    Here's a complete example of how I would do it

    import { Component, OnInit, OnDestroy, DebugElement, Injectable } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { By } from '@angular/platform-browser';
    import { Http, BaseRequestOptions, Response, ResponseOptions } from '@angular/http';
    import { MockBackend, MockConnection } from '@angular/http/testing';
    import { async, fakeAsync, inject, TestBed } from '@angular/core/testing';
    import { Observable } from 'rxjs/Observable';
    import { Subscription } from 'rxjs/Subscription';
    
    @Injectable()
    class BooksService {
      constructor(private http: Http) {}
    
      getBooks(): Observable<string[]> {
        return this.http.get('')
          .map(res => res.json() as string[]);
      }
    }
    
    class MockBooksService {
      subscription: Subscription;
      content;
      error;
    
      constructor() {
        this.subscription = new Subscription();
        spyOn(this.subscription, 'unsubscribe');
      }
      getBooks() {
        return this;
      }
      subscribe(next, error) {
        if (this.content && next && !error) {
          next(this.content);
        }
        if (this.error) {
          error(this.error);
        }
        return this.subscription;
      }
    }
    
    @Component({
      template: `
        <h4 *ngFor="let book of books">{{ book }}</h4>
      `
    })
    class TestComponent implements OnInit, OnDestroy {
      books: string[];
      subscription: Subscription;
    
      constructor(private service: BooksService) {}
    
      ngOnInit() {
        this.subscription = this.service.getBooks().subscribe(books => {
          this.books = books;
        });
      }
    
      ngOnDestroy() {
        this.subscription.unsubscribe();
      }
    }
    
    describe('component: TestComponent', () => {
      let mockService: MockBooksService;
    
      beforeEach(() => {
        mockService = new MockBooksService();
    
        TestBed.configureTestingModule({
          imports: [ CommonModule ],
          declarations: [ TestComponent ],
          providers: [
            { provide: BooksService, useValue: mockService }
          ]
        });
      });
    
      it('should set the books', () => {
        mockService.content = ['Book1', 'Book2'];
        let fixture = TestBed.createComponent(TestComponent);
        fixture.detectChanges();
    
        let debugEls: DebugElement[] = fixture.debugElement.queryAll(By.css('h4'));
        expect(debugEls[0].nativeElement.innerHTML).toEqual('Book1');
        expect(debugEls[1].nativeElement.innerHTML).toEqual('Book2');
      });
    
      it('should unsubscribe when destroyed', () => {
        let fixture = TestBed.createComponent(TestComponent);
        fixture.detectChanges();
        fixture.destroy();
        expect(mockService.subscription.unsubscribe).toHaveBeenCalled();
      });
    });
    
    describe('service: BooksService', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            {
              provide: Http, useFactory: (backend, options) => {
                return new Http(backend, options);
              },
              deps: [MockBackend, BaseRequestOptions]
            },
            MockBackend,
            BaseRequestOptions,
            BooksService
          ]
        });
      });
    
      it('should return mocked content',
        async(inject([MockBackend, BooksService],
                     (backend: MockBackend, service: BooksService) => {
    
        backend.connections.subscribe((conn: MockConnection) => {
          let ops = new ResponseOptions({body: '["Book1", "Book2"]'});
          conn.mockRespond(new Response(ops));
        });
    
        service.getBooks().subscribe(books => {
          expect(books[0]).toEqual('Book1');
          expect(books[1]).toEqual('Book2');
        });
      })));
    });