angular jest测试组件篇

1,703 阅读5分钟

1 组件测试

1.1 无依赖组件

你可以像测试服务类那样来测试一个组件类本身。组件类的测试应该保持非常干净和简单。它应该只测试一个单元。一眼看上去,你就应该能够理解正在测试的对象。考虑这个 LightswitchComponent,当用户单击该按钮时,它会打开和关闭一个指示灯(用屏幕上的一条消息表示)。

// demo.ts
@Component({
  selector: 'lightswitch-comp',
  template: `
    <button (click)="clicked()">Click me!</button>
    <span>{{message}}</span>`
})
export class LightswitchComponent {
  isOn = false;
  clicked() { this.isOn = !this.isOn; }
  get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
// demo.spec.ts
describe('LightswitchComp', () => {
  test('#click 调用', () => {
    const comp = new LightswitchComponent();
    expect(comp.isOn).toBe(false);
    comp.clicked();
    expect(comp.isOn).toBe(true);
    comp.clicked();
    expect(comp.isOn).toBe(false);
  });
  test('#click 设置message', () => {
    const comp = new LightswitchComponent();
    expect(comp.message).toMatch(/is off/i);
    comp.clicked();
    expect(comp.message).toMatch(/is on/i);
  })
});

你可能要测试 clicked() 方法是否切换了灯的开/关状态并正确设置了这个消息。这个组件类没有依赖。要测试这种类型的组件类,请遵循与没有依赖的服务相同的步骤:

  • 使用 new 关键字创建一个组件
  • 调用它的 API
  • 对其公开状态的期望值进行断言

下面是“英雄之旅”教程中的 DashboardHeroComponent。

// demo.ts
@Component({
  selector: 'dashboard-hero',
  template: `
    <div (click)="click()" class="hero">
      {{hero.name | uppercase}}
    </div>`,
  styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
  @Input() hero!: Hero;
  @Output() selected = new EventEmitter<Hero>();
  click() { this.selected.emit(this.hero); }
}
// demo.spec.ts
describe('dashboard-hero', () => {
  test('click时调用selected event', () => {
    const comp = new DashboardHeroComponent();
    const hero = {id: 2, name: 'test'};
    comp.hero = hero;
    comp.selected.pipe(first()).subscribe((selectedHero: Hero) => {
      expect(selectedHero).toBe(hero);
    });
    comp.click();
  })
});

它出现在父组件的模板中,把一个英雄绑定到了 @Input 属性,并监听通过所选 @Output 属性引发的一个事件。你可以测试类代码的工作方式,而无需创建 DashboardHeroComponent 或它的父组件。

1.2 有依赖组件

当组件有依赖时,你可能要使用 TestBed 来同时创建该组件及其依赖。下列的WelcomeComponent 依赖于 UserService 来了解要问候的用户的名字。你可以先创建一个能满足本组件最低需求的 UserService,然后在 TestBed 配置中提供并注入所有这些组件和服务。然后,测验组件类,别忘了要像 Angular 运行应用时一样调用生命周期钩子方法。

// use.service.ts
@Injectable()
export class UserService {
  isLoggedIn = true;
  user = {name: 'Sam Spade'};
}
// welcome.component.ts
@Component({
  selector: 'app-welcome',
  template: '<h3 class="welcome"><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent implements OnInit {
  welcome = '';
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.welcome = this.userService.isLoggedIn ?
      'Welcome, ' + this.userService.user.name : 'Please log in.';
  }
}
// welcome.component.spec.ts
class MockUserService {
  isLoggedIn = true;
  user = { name: 'Test User'};
}

describe('WelcomeComponent', () => {
  let comp: WelcomeComponent;
  let userService: UserService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        WelcomeComponent,
        {provide: UserService, useClass: MockUserService }
      ]
    });
    comp = TestBed.inject(WelcomeComponent);
    userService = TestBed.inject(UserService);
  });

  it('should not have welcome message after construction', () => {
    expect(comp.welcome).toBe('');
  });

  it('should welcome logged in user after Angular calls ngOnInit', () => {
    comp.ngOnInit();
    expect(comp.welcome).toContain(userService.user.name);
  });

  it('should ask user to log in if not logged in after ngOnInit', () => {
    userService.isLoggedIn = false;
    comp.ngOnInit();
    expect(comp.welcome).not.toContain(userService.user.name);
    expect(comp.welcome).toContain('log in');
  });
});

1.3 组件 DOM 测试

测试组件和测试服务一样简单。但组件不仅仅是它的类。组件还会与 DOM 以及其他组件进行交互。只对类的测试可以告诉你类的行为。但它们无法告诉你这个组件是否能正确渲染、响应用户输入和手势,或是集成到它的父组件和子组件中。

当你要求 CLI 生成一个新组件时,它会默认为你创建一个初始的测试文件。比如,下列 CLI 命令会在 app/banner 文件夹中生成带有内联模板和内联样式的 BannerComponent

ng generate component banner --inline-template --inline-style --module app

// 它还会生成一个初始测试文件 `banner.component.spec.ts`,如下所示:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BannerComponent } from './banner.component';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ BannerComponent ]
    })
    .compileComponents();
  });

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

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

由于 compileComponents 是异步的,所以它使用await命令。

只有这个文件的最后三行才是真正测试组件的,并且所有这些都断言了 Angular 可以创建该组件。该文件的其它部分是做设置用的样板代码,可以预见,如果组件演变得更具实质性内容,就会需要更高级的测试。现在,你可以从根本上把这个测试文件减少到一个更容易管理的大小:

describe('BannerComponent (minimal)', () => {
  it('should create', () => {
    TestBed.configureTestingModule({declarations: [BannerComponent]});
    const fixture = TestBed.createComponent(BannerComponent);
    const component = fixture.componentInstance;
    expect(component).toBeDefined();
  });
});

在这个例子中,传给 TestBed.configureTestingModule 的元数据对象只是声明了要测试的组件 BannerComponent

没有必要声明或导入任何其他东西。默认的测试模块预先配置了像来自 @angular/platform-browser 的 BrowserModule 这样的东西。稍后你会用 importsproviders 和更多可声明对象的参数来调用 TestBed.configureTestingModule(),以满足你的测试需求。可选方法 override可以进一步微调此配置的各个方面。

在配置好 TestBed 之后,你就可以调用它的 createComponent() 方法了。

const fixture = TestBed.createComponent(BannerComponent);

TestBed.createComponent() 会创建 BannerComponent 的实例,它把一个对应元素添加到了测试运行器的 DOM 中,并返回一个ComponentFixture 对象。 ComponentFixture 是一个测试挽具,用于与所创建的组件及其对应的元素进行交互。

可以通过测试夹具(fixture)访问组件实例,并用 Jasmine 的期望断言来确认它是否存在:

const component = fixture.componentInstance;
expect(component).toBeDefined();

随着这个组件的发展,你会添加更多的测试。你不必为每个测试复制 TestBed 的配置代码,而是把它重构到 Jasmine 的 beforeEach() 和一些支持变量中:

describe('BannerComponent (with beforeEach)', () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({declarations: [BannerComponent]});
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance;
  });

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

现在添加一个测试程序,它从 fixture.nativeElement 中获取组件的元素,并查找预期的文本。

it('should contain "banner works!"', () => {
  const bannerElement: HTMLElement = fixture.nativeElement;
  expect(bannerElement.textContent).toContain('banner works!');
});

ComponentFixture.nativeElement 的值是 any 类型的。稍后你会遇到 DebugElement.nativeElement,它也是 any 类型的。

Angular 在编译时不知道 nativeElement 是什么样的 HTML 元素,甚至可能不是 HTML 元素。该应用可能运行在非浏览器平台(如服务器或 Web Worker,在那里本元素可能具有一个缩小版的 API,甚至根本不存在。我们的测试都是为了在浏览器中运行而设计的,因此 nativeElement 的值始终是 HTMLElement 或其派生类之一。

知道了它是某种 HTMLElement ,就可以用标准的 HTML querySelector 深入了解元素树。这是另一个调用 HTMLElement.querySelector 来获取段落元素并查找横幅文本的测试:

test('should have <p> with "banner works!"', () => {
  const bannerElement: HTMLElement = fixture.nativeElement;
  const p = bannerElement.querySelector('p')!;
  expect(p.textContent).toEqual('banner works!');
});