💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
[TOC] 当`@ionic/angular`使用离子CLI生成的应用程序,它是使用茉莉自动建立到单元测试的应用程序。这与Angular CLI使用的设置相同。有关单元测试角度应用程序的详细信息,请参阅[角度测试指南](https://angular.io/guide/testing)。 ## 测试原理 在测试应用程序时,最好记住测试的目的不是验证代码是否正确,而是发现代码问题,因此当修复程序更便宜时,可以在开发周期的早期解决错误。这是一个微妙但重要的区别。 如果我们打算证明代码是正确的,我们更有可能坚持通过代码的快乐路径。如果我们开始寻找问题,我们更有可能更充分地运用代码并找到潜伏在那里的错误。 ## 一般测试结构 单元测试包含在`spec`文件中,`spec`每个实体有一个文件(组件,页面,服务,管道等)。这些`spec`文件与他们正在测试的源并排,并以其命名。例如,如果项目具有名为WeatherService的服务,则其代码将位于以名为的文件中`weather.service.ts`的测试命名的文件中`weather.service.spec.ts`。这两个文件都在同一个文件夹中。 该`spec`文件本身包含一个`describe`电话,它定义了全面的测试。嵌套在其中的是`describe`定义主要功能区域的其他调用。每个`describe`调用可以包含设置和拆除代码(通常通过`beforeEach`和`afterEach`调用处理),更多`describe`调用形成功能的分层细分,以及`it`定义各个测试用例的调用。 该`describe`和`it`电话还包含一个描述性文本标签。在格式良好的测试中,`describe`和`it`调用与其标签组合以执行正确的短语,并且通过组合`describe`和`it`标签形成的每个测试用例的完整标签创建完整的句子。 例如: ~~~ describe('Calculation', () => { describe('divide', () => { it('calculates 4 / 2 properly' () => {}); it('cowardly refuses to divide by zero' () => {}); ... }); describe('multiply', () => { ... }); }); ~~~ 外部`describe`调用表明`Calculation`正在测试服务,内部`describe`调用确切地说明了正在测试的功能,并且`it`调用说明测试用例是什么。运行时,每个测试用例的完整标签是一个有意义的句子(计算除以懦弱拒绝除以零)。 ## 页面和组件 页面只是Angular组件。因此,页面和组件都使用[Angular的组件测试](https://angular.io/guide/testing#component-test-basics)指南进行[测试](https://angular.io/guide/testing#component-test-basics)。 由于页面和组件包含TypeScript代码和HTML模板标记,因此可以执行组件类测试和组件DOM测试。创建页面时,生成的模板测试如下所示: ~~~ import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TabsPage } from './tabs.page'; describe('TabsPage', () => { let component: TabsPage; let fixture: ComponentFixture<TabsPage>; beforeEach(async () => { TestBed.configureTestingModule({ declarations: [TabsPage], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(TabsPage); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ~~~ 在进行组件类测试时,使用通过定义的组件对象访问组件对象`component = fixture.componentInstance;`。这是组件类的实例。在进行DOM测试时,使用该`fixture.nativeElement`属性。这是`HTMLElement`组件的实际情况,它允许测试使用标准HTML API方法,例如`HTMLElement.querySelector`为了检查DOM。 ## 服务 服务通常分为两大类:执行计算和其他操作的实用程序服务,以及主要执行HTTP操作和数据操作的数据服务。 ### 基本服务测试 测试大多数服务的建议方法是实例化服务并为服务具有的任何依赖项手动注入模拟。这样,代码可以单独测试。 假设有一种方法可以获取一组时间卡并计算净工资。我们还假设税收计算是通过当前服务所依赖的另一项服务来处理的。这个工资单服务可以这样测试: ~~~ import { PayrollService } from './payroll.service'; describe('PayrollService', () => { let service: PayrollService; let taxServiceSpy; beforeEach(() => { taxServiceSpy = jasmine.createSpyObj('TaxService', { federalIncomeTax: 0, stateIncomeTax: 0, socialSecurity: 0, medicare: 0 }); service = new PayrollService(taxServiceSpy); }); describe('net pay calculations', () => { ... }); }); ~~~ 这允许测试通过模拟设置控制各种税收计算返回的值,例如`taxServiceSpy.federalIncomeTax.and.returnValue(73.24)`。这允许“净工资”测试独立于税收计算逻辑。当税码发生变化时,只需要更改与税务服务相关的代码和测试。净工资的测试可以继续按原样运行,因为这些测试不关心如何计算税收,只是正确地应用了价值。 通过`ionic g service name`使用Angular的测试实用程序生成服务时使用的脚手架并设置测试模块。这样做并非绝对必要。但是,该代码可以保留,允许手动构建服务或者按原样注入: ~~~ import { TestBed, inject } from '@angular/core/testing'; import { PayrollService } from './payroll.service'; import { TaxService } from './tax.service'; describe('PayrolService', () => { let taxServiceSpy; beforeEach(() => { taxServiceSpy = jasmine.createSpyObj('TaxService', { federalIncomeTax: 0, stateIncomeTax: 0, socialSecurity: 0, medicare: 0 }); TestBed.configureTestingModule({ providers: [ PayrollService, { provide: TaxService, useValue: taxServiceSpy } ] }); }); it('does some test where it is injected', inject([PayrollService], (service: PayrollService) => { expect(service).toBeTruthy(); }) ); it('does some test where it is manually built', () => { const service = new PayrollService(taxServiceSpy); expect(service).toBeTruthy(); }); }); ~~~ ### 测试HTTP数据服务 执行HTTP操作的大多数服务将使用Angular的HttpClient服务来执行这些操作。对于此类测试,建议使用Angular`HttpClientTestingModule`。有关此模块的详细文档,请参阅Angular的[Angular的测试HTTP请求](https://angular.io/guide/http#testing-http-requests)指南。 这种测试的基本设置如下所示: ~~~ import { HttpBackend, HttpClient } from '@angular/common/http'; import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; import { TestBed, inject } from '@angular/core/testing'; import { IssTrackingDataService } from './iss-tracking-data.service'; describe('IssTrackingDataService', () => { let httpClient: HttpClient; let httpTestingController: HttpTestingController; let issTrackingDataService: IssTrackingDataService; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ IssTrackingDataService ] }); httpClient = TestBed.get(HttpClient); httpTestingController = TestBed.get(HttpTestingController); issTrackingDataService = new IssTrackingDataService(httpClient); }); it('exists', inject([IssTrackingDataService], (service: IssTrackingDataService) => { expect(service).toBeTruthy(); })); describe('location', () => { it('gets the location of the ISS now', () => { issTrackingDataService.location().subscribe(x => { expect(x).toEqual({ longitude: -138.1719, latitude: 44.4423 }); }); const req = httpTestingController.expectOne( 'http://api.open-notify.org/iss-now.json' ); expect(req.request.method).toEqual('GET'); req.flush({ iss_position: { longitude: '-138.1719', latitude: '44.4423' }, timestamp: 1525950644, message: 'success' }); httpTestingController.verify(); }); }); }); ~~~ ## 管道 管道就像具有明确定义的接口的服务。它是一个包含一个公共方法的类`transform`,它操纵输入值(和其他可选参数)以创建在页面上呈现的输出。测试管道:实例化管道,调用transform方法,并验证结果。 举个简单的例子,让我们看一个带有`Person`对象并格式化名称的管道。为了简单起见,我们假设一个`Person`由一`id`,`firstName`,`lastName`,和`middleInitial`。管道的要求是将名称打印为“Last,First M.”处理不存在名字,姓氏或中间名字的情况。这样的测试可能如下所示: ~~~ import { NamePipe } from './name.pipe'; import { Person } from '../../models/person'; describe('NamePipe', () => { let pipe: NamePipe; let testPerson: Person; beforeEach(() => { pipe = new NamePipe(); testPerson = { id: 42, firstName: 'Douglas', lastName: 'Adams', middleInitial: 'N' }; }); it('exists', () => { expect(pipe).toBeTruthy(); }); it('formats a full name properly', () => { expect(pipe.transform(testPerson)).toBeEqual('Adams, Douglas N.'); }); it('handles having no middle initial', () => { delete testPerson.middleInitial; expect(pipe.transform(testPerson)).toBeEqual('Adams, Douglas'); }); it('handles having no first name', () => { delete testPerson.firstName; expect(pipe.transform(testPerson)).toBeEqual('Adams N.'); }); it('handles having no last name', () => { delete testPerson.lastName; expect(pipe.transform(testPerson)).toBeEqual('Douglas N.'); }); }); ~~~ 在利用管道的组件和页面中通过DOM测试来锻炼管道也是有益的。 ## 使用模拟 通常最好单独测试代码模块。为此,请使用模拟对象(Jasmine称之为“间谍”)代替依赖项。使用模拟对象时,测试可以控制对该依赖项的调用返回的值,使当前测试独立于对依赖项所做的更改。这也使测试设置更容易,允许测试仅关注被测模块中的代码。 使用模拟还允许测试查询模拟以确定它被调用以及如何通过`toHaveBeenCalled*`函数集调用它。测试应该尽可能具体,使用这些函数,有利于在测试已调用方法时调用`toHaveBeenCalledTimes`过度调用`toHaveBeenCalled`。那`expect(mock.foo).toHaveBeenCalledTimes(1)`比做得好`expect(mock.foo).toHaveBeenCalled()`。在测试未调用的东西时应该遵循相反的建议(`expect(mock.foo).not.toHaveBeenCalled()`)。 在Jasmine中创建模拟对象有两种常用方法。模拟对象可以从头开始使用来构造`jasmine.createSpy`和`jasmine.createSpyObj`或间谍可以利用被安装到现有的物体`spyOn()`和`spyOnProperty()`。 ### 使用`jasmine.createSpy`和`jasmine.createSpyObj` `jasmine.createSpyObj`从头开始创建一个完整的模拟对象,并在创建时定义一组模拟方法。这很有用,因为它非常简单。没有任何东西需要构建或注入测试。使用此功能的缺点是它允许创建可能与真实对象不匹配的对象。 `jasmine.createSpy`是类似的,但它创建了一个独立的模拟功能。 ### 使用`spyOn()`和`spyOnProperty()` `spyOn()`在现有对象上安装间谍。使用此技术的优点是,如果尝试监视对象上不存在的方法,则会引发异常。这可以防止测试模拟不存在的方法。缺点是测试需要一个完全形成的对象,这可能会增加所需的测试设置量。 `spyOnProperty()`类似的不同之处在于它间谍属性而不是方法。