4

我有以下 Angular 2.0.0 组件:

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());
  }

}

我将如何测试该ngOnInit()功能?

我不想包括到目前为止我尝试过的测试,因为我怀疑我偏离了正确的轨道,我不想让答案产生偏见。

4

2 回答 2

1

要模拟Http ,您需要MockBackend在. 然后您可以订阅它的连接并提供模拟响应HttpTestBed

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

我将如何测试 ngOnInit() 函数?

问题是Http.get调用的异步性质。ngOnInit调用时会被调用fixture.detectChanges(),但是 Http 的异步特性导致测试在 Http 完成之前运行。为此我们使用,正如这里fakeAsync提到的,但下一个问题是您不能使用and 。你可以用 a 破解它并在那里测试。那会奏效。我个人不喜欢它。fakeAsynctemplateUrlsetTimeout

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

就个人而言,我认为你一开始就有设计缺陷。Http调用应该被抽象成一个服务,并且组件应该与服务交互,而不是直接与Http. 如果您将设计更改为此(我会推荐),那么您可以像本示例中那样测试服务,并且对于组件测试,创建一个同步模拟,如本文所述。

这是我将如何做的完整示例

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');
    });
  })));
});
于 2016-09-20T16:23:02.473 回答
1

我实际上最终做了一些不同的事情。

根据 peeskillet 的建议,我重构了代码以使用服务。这是服务的样子。

// src/app/book.service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class BookService {

  constructor(private http: Http) { }

  getList() {
    return this.http.get('/api/books.json');
  }
}

BookListComponent这是使用该服务的修改。

// book-list.component.ts

import { Component, OnInit } from '@angular/core';
import { BookService } from '../book.service';

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

  constructor(private bookService: BookService) { }

  ngOnInit() {
    this.bookService.getList()
      .subscribe(response => this.books = response.json());
  }

}

最后,这是工作测试。

// book-list.component.spec.ts

/* tslint:disable:no-unused-variable */

import { TestBed, async } from '@angular/core/testing';
import { MockBackend } from '@angular/http/testing';
import { Observable } from 'rxjs/Observable';

import {
  Http,
  Response,
  ResponseOptions,
  BaseRequestOptions,
  ConnectionBackend
} from '@angular/http';

import { BookListComponent } from './book-list.component';
import { BookService } from '../book.service';

describe('Component: BookList', () => {
  let fixture;
  let component;
  let bookService;
  let spy;
  let testList;

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

    fixture = TestBed.createComponent(BookListComponent);
    component = fixture.debugElement.componentInstance;

    bookService = fixture.debugElement.injector.get(BookService);

    let observable: Observable<Response> = Observable.create(observer => {
      let responseOptions = new ResponseOptions({
        body: '[{ "name": "Whiteboard Interviews" }]'
      });

      observer.next(new Response(responseOptions));
    });

    spy = spyOn(bookService, 'getList')
      .and.returnValue(observable);
  });

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

  it('should return a response', () => {
    fixture.detectChanges();
    expect(component.books).toEqual([
      { 'name': 'Whiteboard Interviews' }
    ]);
  });
});
于 2016-09-21T14:05:34.087 回答