我应该如何对已导入其他模块(和组件)的 Angular 2 组件和模块进行单元测试

How should I Unit test Angular2 components and modules that has imported other modules (and components)

我花了一段时间来掌握 Angular2 中的模块,并且非常喜欢它们,但是对于测试我的模块和其中的组件的最佳方法,我有点不确定。 (我也意识到我的 app.component 可以而且可能应该被分解更多,但现在它有助于学习测试框架更复杂一点)

例如这是我的 app.module:

import { createStore, compose, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk';
import { AUTH_PROVIDERS } from 'angular2-jwt';

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { ComponentsModule } from './components';
import { MaterialModule} from './material';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { ViewsModule } from './+views';

import { rootReducer } from './dataStore';

import { CurrentUserModel } from './models/current-user'


const appStore = createStore(rootReducer, applyMiddleware(ReduxThunk));

const APP_DECLARATIONS = [
  AppComponent
];

const APP_PROVIDERS = [
  { provide: 'AppStore', useValue: appStore },
  CurrentUserModel
];

@NgModule({
  imports:[
    FormsModule,
    BrowserModule,
    RouterModule,// here as well as in our Views Module because of router-outlet
    ViewsModule,
    MaterialModule, // here as well as in our Views & componet Module because used in App componet
    ComponentsModule
  ],
  declarations: APP_DECLARATIONS,
  bootstrap:[AppComponent],
  providers: APP_PROVIDERS,
})
export class AppModule {

}

这就是我的 app.component 的样子:

import { Component, ViewChild, AfterViewInit } from '@angular/core';

import { Router } from '@angular/router';

@Component({
  selector: 'app',
  styleUrls:['app.component.scss'],
  template: `
  <md-toolbar>
  <!--  <i class="material-icons demo-toolbar-icon">menu</i> -->
    <span class="toolbar-brand">Franks</span>
    <span *ngIf="searchActive" role="search" class="fill-remaining-space">
        <span class="search-input-container flex flex-1">
          <i class="material-icons search-link">search</i>
          <input class="search-input" placeholder="Search" type="text" id="searchInput" #searchInput (keyup.esc)="exitSearch($event)"/>
        </span>
    </span>
    <i *ngIf="searchActive" class="material-icons right selectable" (click)="exitSearch($event)">close</i>

    <span *ngIf="!searchActive" class="fill-remaining-space">

    </span>
    <span *ngIf="!searchActive" role="navmenu">
      <span class="hlink" routerLink="/" routerLinkActive="active">home</span>
      <span class="hlink" routerLink="/profile" routerLinkActive="active">Profile</span>
      <span class="hlink" routerLink="/login" routerLinkActive="active">Login</span>
      <span class="hlink" routerLink="/signup" routerLinkActive="active">Sign Up</span>
      <i class="material-icons search-link" (click)="activeSearch($event)">search</i>
    </span>
  </md-toolbar>
  <div class="container">
    <router-outlet></router-outlet>
  </div>
  `,
})
export class AppComponent {
  @ViewChild('searchInput') searchInputRef;

  ngAfterViewChecked() {
    if(this.searchActive && this.searchInputRef){
      console.log(this.searchInputRef);       
      this.searchInputRef.nativeElement.focus();
    }
  }
  searchActive: boolean;
  constructor(public router: Router) {
    this.searchActive = false;
  }

  activeSearch(event):void {
    this.searchActive = true;
  }

  exitSearch(event) : void {
    this.searchActive = false;
  }
}

所以我知道我可以在我的测试中模拟所有组件,例如 MaterialComponents(这只是 material 组件的包装模块)但这看起来有点乏味.这是我唯一的选择吗?如果是的话,在创建组件过程的一部分时创建组件模拟是否有意义。

出于参考目的,这是我的 material 模块的样子,我的视图和组件模块类似:

import { NgModule } from '@angular/core';

// Material
import { MdCardModule } from '@angular2-material/card';
import { MdButtonModule } from '@angular2-material/button';
import { MdInputModule } from '@angular2-material/input';
import { MdToolbarModule } from '@angular2-material/toolbar';
import { MdListModule } from '@angular2-material/list';
import { MdIconModule, MdIconRegistry } from '@angular2-material/icon';



const MATERIAL_UI_MODULES = [
  MdCardModule,
  MdButtonModule,
  MdInputModule,
  MdToolbarModule,
  MdIconModule,
  MdListModule
]
const MATERIAL_UI_REGISTRIES = [
  MdIconRegistry
]

@NgModule({
  imports:[
    ...MATERIAL_UI_MODULES,
  ],
  providers: MATERIAL_UI_REGISTRIES,
  exports:[
    ...MATERIAL_UI_MODULES,
  ]
})
export class MaterialModule {

}

所以我最终想出了如何做到这一点,这是我目前的解决方案:

/* tslint:disable:no-unused-variable */
import { TestBed, inject, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';

import { Component,ViewChild, AfterViewChecked  } from '@angular/core';
import { Router } from '@angular/router';
import { Location, CommonModule } from '@angular/common';
import { MaterialModule} from './material';

@Component({
  template: '<div></div>'
})
class DummyComponent {
}


import { AppComponent } from './app.component';

describe('component: TestComponent', function () {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        CommonModule,
        RouterTestingModule.withRoutes([
         { path: 'profile', component: DummyComponent },
         { path: 'login', component: DummyComponent },
         { path: 'signup', component: DummyComponent },
         { path: '', component: DummyComponent }
       ]),
       MaterialModule
      ],
      declarations: [ AppComponent, DummyComponent ]
    });
  });

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

  it('should be navigate to correct url for each option in navmenu',
    async(inject([Router, Location], (router: Router, location: Location) => {

    let fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();

    fixture.debugElement.query(By.css('span.hlink[routerLink="/profile"]')).nativeElement.click();
    fixture.whenStable().then(() => {
      expect(location.path()).toEqual('/profile');
      expect(fixture.debugElement.query(By.css('span.hlink[routerLink="/profile"]')).classes['active']).toBeTruthy();
      expect(fixture.debugElement.nativeElement.querySelectorAll('span.hlink.active').length).toEqual(1)

      fixture.debugElement.query(By.css('span.hlink[routerLink="/login"]')).nativeElement.click();
      return fixture.whenStable();
    }).then(() => {
      expect(location.path()).toEqual('/login');
      expect(fixture.debugElement.query(By.css('span.hlink[routerLink="/login"]')).classes['active']).toBeTruthy();
      expect(fixture.debugElement.nativeElement.querySelectorAll('span.hlink.active').length).toEqual(1)

      fixture.debugElement.query(By.css('span.hlink[routerLink="/signup"]')).nativeElement.click();
      return fixture.whenStable();
    }).then(() => {
      expect(location.path()).toEqual('/signup');
      expect(fixture.debugElement.query(By.css('span.hlink[routerLink="/signup"]')).classes['active']).toBeTruthy();
      expect(fixture.debugElement.nativeElement.querySelectorAll('span.hlink.active').length).toEqual(1)

      fixture.debugElement.query(By.css('span.hlink[routerLink="/"]')).nativeElement.click();
      return fixture.whenStable();
    }).then(() => {
      expect(location.path()).toEqual('/');
      expect(fixture.debugElement.query(By.css('span.hlink[routerLink="/"]')).classes['active']).toBeTruthy();
      expect(fixture.debugElement.nativeElement.querySelectorAll('span.hlink.active').length).toEqual(1)
    });

  })));
});