Angular 单元测试 101(含示例)在开始 Angular 自动化测试之前总结

2025-06-07

Angular 单元测试 101(含示例)

在我们开始之前

角度自动化测试

包起来

我们为软件添加的功能越多,其复杂性就越高。随着复杂性的增加,手动测试所需的时间也随之增加。事实上,随着我们向应用程序添加新功能,手动测试所需的时间会呈指数级增长!
为了避免这种情况,我们可以利用自动化测试,因为它是提高应用程序测试有效性、效率和覆盖率的最佳方法。

在本文中,我们将讨论如何使用 Karma 和 Jasmine 进行 Angular 单元测试。读完本文后,你应该能够轻松地编写规范来测试 Angular 组件、指令、管道和服务,并学习测试同步和异步行为的技巧。

在我们开始之前

首先,我们来聊聊测试的一些基础知识和术语。这有助于我们建立起对测试工作原理的认知模型,从而更好地理解后面的内容。

术语

自动化测试

这是编写代码来测试代码,然后运行这些测试的实践。测试分为三种类型:单元测试、集成测试和端到端 (e2e) 测试。

单元测试

单元测试或 UT 是检查软件特定部分或程序某一部分是否正常运行的过程。

因果

Karma 是一个测试运行器。它会自动创建一个浏览器实例,运行我们的测试,然后返回结果。它最大的优势在于,它允许我们在不同的浏览器中测试代码,而无需我们进行任何手动更改。

Karma 用于识别测试文件的模式是<filename>.spec.ts。这是其他语言使用的通用约定。如果您出于某种原因想要更改它,可以在test.ts文件中进行更改。

茉莉花

Jasmine 是一个流行的 JavaScript 测试框架。它通过间谍程序(我们稍后会定义什么是间谍程序)实现了测试替身,并且内置了开箱即用的断言功能。

Jasmine 提供了很多编写测试的实用函数。主要的三个 API 如下:

  1. Describe():这是一套测试
  2. it():单次测试声明
  3. expect():例如期望某事为真

嘲笑

模拟对象是虚假的(模拟的)对象,以受控的方式模仿真实对象的行为。

固定装置

Fixture 是组件实例的包装器。通过 Fixture,我们可以访问组件实例及其模板。

间谍

Spies 有助于验证组件依赖外部输入的行为,而无需定义这些外部输入。它们在测试依赖服务的组件时非常有用。

基础知识

Angular CLI 会下载并安装使用 Jasmine 测试框架测试 Angular 应用所需的一切。只需运行以下命令即可开始测试:



ng test


Enter fullscreen mode Exit fullscreen mode

此命令在监视模式下构建应用程序并启动 Karma。

角度自动化测试

测试框架

使用上面提到的三个 Jasmine API,单元测试的框架应该如下所示:



describe('TestSuitName', () => {
  // suite of tests here

  it('should do some stuff', () => {
    // this is the body of the test
  });
});


Enter fullscreen mode Exit fullscreen mode

在测试中,有一种模式几乎成为了整个开发者社区的标准,叫做 AAA(安排-执行-断言)。AAA 建议你将测试方法分为三个部分:安排、执行和断言。每个部分只负责其命名对应的部分。

因此,在安排部分,您只需要设置特定测试所需的代码。在这里,您将创建对象、设置模拟(如果您使用的话)并设置潜在的期望值。然后是 Act,它应该是被测试方法的调用。在 Assert 部分,您只需检查期望值是否得到满足即可。

遵循这种模式确实使代码结构良好且易于理解。一般来说,它看起来是这样的:



  it('should truncate a string if its too long (>20)', () => {
    // Arrange
    const pipe = new TroncaturePipe();

    // Act
    const ret = pipe.transform('1234567890123456789012345');

    // Assert
    expect(ret.length).toBeLessThanOrEqual(20);
  });


Enter fullscreen mode Exit fullscreen mode

配置和实例

为了访问我们想要测试的组件的方法,我们首先需要实例化它。Jasmine
提供了一个名为 的 API,beforeAll()它会在所有测试之前调用一次。
问题是,如果我们在这个函数中实例化组件,我们的测试将不会被隔离,因为组件属性可能会被每个测试更改,因此第一个测试可能会影响第二个测试的行为。
为了解决这个问题,Jasmine 还有另一个名为 的 API beforeEach(),它非常有用,因为它允许我们的测试从同一个起点运行,从而实现隔离运行。
因此,使用这个 API,我们的测试应该如下所示:



describe('componentName', () => {
  // suite of tests here

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [myComponent],
    });

    fixture = TestBed.createComponent(myComponent);
    component = fixture.componentInstance;
  });

  it('should do some stuff', () => {
    // this is the body of the test

    // test stuff here
    expect(myComponent.methodOfMyComponent()).not.toBe(true);
  });
});


Enter fullscreen mode Exit fullscreen mode

突然间,我们涌现出一大堆未知的 API。让我们仔细看看这些 API 都有哪些。Angular
自带了一个测试 API,testBed其中包含一个配置测试模块的方法,configureTestingModule()我们可以在其中导入其他 Angular 模块、组件、管道、指令或服务。
配置好测试模块后,我们就可以实例化想要测试的组件了。

成分

Angular 组件结合了 HTML 模板和 TypeScript 类。
因此,要测试组件,我们需要在浏览器 DOM 中创建组件的宿主元素。
为此,我们使用一个TestBed名为 的方法createComponent()。此方法将创建一个包含组件实例及其 HTML 引用的 Fixture。使用此 Fixture,我们可以通过 调用其属性及其 HTML 引用
来访问原始组件componentInstancenativeElement

这样,Angular 组件测试应该如下所示:



describe('HeaderComponent', () => {
  let component: HeaderComponent;
  let element: HTMLElement;
  let fixture: ComponentFixture<HeaderComponent>;

  // * We use beforeEach so our tests are run in isolation
  beforeEach(() => {
    TestBed.configureTestingModule({
      // * here we configure our testing module with all the declarations,
      // * imports, and providers necessary to this component
      imports: [CommonModule],
      providers: [],
      declarations: [HeaderComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(HeaderComponent);
    component = fixture.componentInstance; // The component instantiation 
    element = fixture.nativeElement; // The HTML reference
  });

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

  it('should create', () => {
    // * arrange
    const title = 'Hey there, i hope you are enjoying this article';
    const titleElement = element.querySelector('.header-title');
    // * act
    component.title = title;
    fixture.detectChanges(); 
    // * assert
    expect(titleElement.textContent).toContain(title);
  });
});


Enter fullscreen mode Exit fullscreen mode

在测试中设置标题后,我们需要调用 detectChanges() 以便使用我们刚刚设置的新标题更新模板(因为绑定发生在 Angular 执行变更检测时)。

管道

因为管道是一个具有一个方法 transform 的类(将输入值转换为转换后的输出值),所以无需任何 Angular 测试实用程序即可更轻松地进行测试。

下面是管道测试的示例:



describe('TroncaturePipe', () => {
  it('create an instance', () => {
    const pipe = new TroncaturePipe(); // * pipe instantiation
    expect(pipe).toBeTruthy();
  });

  it('truncate a string if its too long (>20)', () => {
    // * arrange
    const pipe = new TroncaturePipe();
    // * act
    const ret = pipe.transform('123456789123456789456666123');
    // * asser
    expect(ret.length).toBe(20);
  });
});



Enter fullscreen mode Exit fullscreen mode

指令

属性型指令会修改元素的行为。因此,您可以像管道一样对其进行单元测试,只测试其方法;或者,您也可以使用宿主组件进行测试,检查它是否正确地改变了自身行为。

以下是使用主机组件测试指令的示例:



// * Host component:
@Component({
  template: `<div [appPadding]="2">Test</div>`,
})
class HostComponent {}
@NgModule({
  declarations: [HostComponent, PaddingDirective],
  exports: [HostComponent],
})
class HostModule {}

// * Test suite:
describe('PaddingDirective', () => {
  let component: HostComponent;
  let element: HTMLElement;
  let fixture: ComponentFixture<HostComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [CommonModule, HostModule], // * we import the host module
    }).compileComponents();

    fixture = TestBed.createComponent(HostComponent);
    component = fixture.componentInstance;
    element = fixture.nativeElement;

    fixture.detectChanges(); // * so the directive gets appilied
  });

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

  it('should add padding', () => {
    // * arrange
    const el = element.querySelector('div');
    // * assert
    expect(el.style.padding).toBe('2rem'); // * we check if the directive worked correctly
  });
});


Enter fullscreen mode Exit fullscreen mode

服务

与管道类似,服务通常更容易测试。我们可以使用new关键字来实例化它们。对于基本服务来说,这没问题,但如果您的服务有依赖项,最好像TestBed.configureTestingModule这样使用 API:



describe('LocalService', () => {
  let service: LocalService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [LocalService],
    });

    service = TestBed.inject(LocalService); // * inject service instance
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should set the local', () => {
    // * act
    service.setLocal('fr');
    // * assert
    expect(service.getLocal()).toBe('fr');
  });
});


Enter fullscreen mode Exit fullscreen mode

TestBed.get()为了获取服务实例,我们可以通过调用(以服务类作为参数)将其注入测试中。

好了,现在你应该可以为你的 Angular 应用编写测试了。为了解决你在编写 Angular 测试时可能遇到的一些常见问题,我添加了一些小抄,你可以在下一节中找到 :)

备忘单

处理 HTTP 请求

为了避免每次测试都发起 HTTP 请求,一种方法是提供一个模拟服务来模拟真实服务(通过 HTTP 请求进行通信的服务)。
实现模拟服务后,我们将其提供给TestBed.configureTestingModule()如下代码:



class FakeApiService {
  // Implement the methods you want to overload here
  getData() {
    return of({ items: [] }); // * mocks the return of the real method
  }
}
//...
TestBed.configureTestingModule({
  imports: [],
  declarations: [myComponent],
  providers: [
    {
      provide: RealApiService,
      useClass: FakeApiService,
    },
  ],
});
//...


Enter fullscreen mode Exit fullscreen mode

处理 Angular 路由器

为了处理路由器,您可以RouterTestingModule在测试模块的导入中添加它,也可以使用我们在上面的测试中看到的技术来模拟它。

使用间谍

间谍是一种简单的方法,可以用来检查某个函数是否被调用,或者提供自定义的返回值。
以下是如何使用它们的示例:



it('should do something', () => {
  // arrange
  const service = TestBed.get(dataService);
  const spyOnMethod = spyOn(service, 'saveData').and.callThrough();
  // act
  component.onSave();
  // assert
  expect(spyOnMethod).toHaveBeenCalled();
});


Enter fullscreen mode Exit fullscreen mode

您可以在这篇精彩的文章中阅读有关间谍的更多信息。

处理异步代码

值得注意的是,自从我写了这篇文章以来,已经出现了一些新的和改进的异步代码测试方法。我将在以后的文章中继续讨论这个主题。

处理承诺


it('should do something async', async () => {
  //  * arrange
  const ob = { id: 1 };
  component.selected = ob;
  //  * act
  const selected = await component.getSelectedAsync(); // get the promise value
  //  * assert
  expect(selected.id).toBe(ob.id);
});


Enter fullscreen mode Exit fullscreen mode
处理可观察对象


it('should do something async', (done) => {
  //  * arrange
  const ob = { id: 1 };
  component.selected = ob;
  //  * act
  const selected$ = component.getSelectedObs(); // get an Observable
  //  * assert
  selected$.subscribe(selected => {
    expect(selected.id).toBe(ob.id);
    done(); // let Jasmine know that you are done testing
  });
});


Enter fullscreen mode Exit fullscreen mode
处理超时


const TIMEOUT_DELAY = 250;
//...
it('should do something async', (done) => {
  //  * arrange
  const ob = { id: 1 };
  //  * act
  component.setSelectedAfterATimeout(ob);
  // * assert
  setTimeout(() => {
    expect(component.selected.id).toBe(ob.id);
    done(); // let Jasmine know that you are done testing
  }, TIMEOUT_DELAY);
});


Enter fullscreen mode Exit fullscreen mode

包起来

结论

在本文中,我们了解了 Angular CLI 为我们配置了一切,我们只需运行它ng test即可启动测试。然后,我们了解了什么是自动化测试,以及如何使用 Jasmine 和 Angular 测试实用程序(针对组件、管道、指令和服务)编写自动化测试。最后,我们还看到了编写测试时可能遇到的一些特殊情况的示例。

以上只是对 Angular 测试的粗略介绍,还有很多内容需要学习。正因如此,本文才成为“Angular 测试”系列的第一篇。欢迎在 Twitter 上关注我@theAngularGuy,获取最新资讯,了解后续文章的发布时间。

与此同时,祝您编码愉快!


接下来读什么?

文章来源:https://dev.to/mustapha/angular-unit-testing-101-with-examples-6mc
PREV
7 种截取代码的绝妙方法
NEXT
对于初学者 - 通过构建 Python 键盘记录器来分析您自己的日常活动👽