测试
当一个@betway东盟体育appionic /角
应用程序是使用Ionic CLI生成的,它会自动设置为单元测试和应用程betway东盟体育app序的端到端测试。这和Angular CLI使用的设置是一样的。请参阅Angular测试指南查看关于测试Angular应用程序的详细信息。
测试的原则
在测试应用程序时,最好记住测试可以显示系统中是否存在缺陷。然而,不可能证明任何非平凡系统是完全没有缺陷的。因此,测试的目标不是验证代码是否正确,而是在代码中发现问题。这是一个微妙但重要的区别。
如果我们开始证明代码是正确的,我们更有可能坚持通过代码的快乐路径。如果我们开始寻找问题,我们更有可能更全面地练习代码,并发现潜伏在那里的bug。
最好从一开始就开始测试应用程序。这样就可以在过程的早期发现缺陷,此时缺陷更容易修复。这还允许在向系统添加新特性时自信地重构代码。
单元测试
单元测试测试一个独立于系统其余部分的代码单元(组件、页面、服务、管道等)。隔离是通过注入模拟对象来代替代码的依赖项来实现的。模拟对象允许测试对依赖项的输出进行细粒度控制。模拟还允许测试确定调用了哪些依赖项以及向它们传递了什么。
编写良好的单元测试的结构是这样的:代码单元及其包含的特性是通过描述的描述()
回调。代码单元的需求及其特性是通过它()
回调。当描述为描述()
而且它()
回调函数被读取,它们作为一个短语是有意义的。当描述为嵌套时描述()
S和final它()
连接在一起,它们形成一个完整描述测试用例的句子。
由于单元测试孤立地执行代码,因此它们快速、健壮,并允许高度的代码覆盖。
使用模拟
单元测试孤立地测试代码模块。为了实现这一点,我们建议使用Jasmine (https://jasmine.github.io/).Jasmine在测试时创建模拟对象(Jasmine称之为“间谍”)来代替依赖项。当使用模拟对象时,测试可以控制调用该依赖项返回的值,使当前测试独立于对依赖项所做的更改。这也使得测试设置更容易,允许测试只关注被测模块中的代码。
使用模拟还允许测试查询模拟,以确定是否调用了它以及如何通过toHaveBeenCalled *
函数的集合。对这些函数的测试应该尽可能具体,有利于对的调用toHaveBeenCalledTimes
通过呼叫toHaveBeenCalled
当测试方法是否被调用时。这是期望(mock.foo) .toHaveBeenCalledTimes (1)
比期望(mock.foo) .toHaveBeenCalled ()
.当测试某个东西没有被调用时,应该遵循相反的建议(期望(mock.foo) .not.toHaveBeenCalled ()
).
在Jasmine中有两种创建模拟对象的常用方法。可以从头构造模拟对象jasmine.createSpy
而且jasmine.createSpyObj
或者间谍可以安装到现有的对象使用spyOn ()
而且spyOnProperty ()
.
使用jasmine.createSpy
而且jasmine.createSpyObj
jasmine.createSpyObj
从头使用创建时定义的一组模拟方法创建完整的模拟对象。这很有用,因为它非常简单。测试中不需要构造或注入任何东西。使用此函数的缺点是它允许创建可能与实际对象不匹配的对象。
jasmine.createSpy
类似,但它创建了一个独立的模拟函数。
使用spyOn ()
而且spyOnProperty ()
spyOn ()
在现有对象上安装间谍。使用这种技术的优点是,如果试图监视对象上不存在的方法,则会引发异常。这可以防止测试模拟不存在的方法。缺点是测试开始时需要一个完全成形的对象,这可能会增加所需的测试设置量。
spyOnProperty ()
类似,区别在于它监视的是属性而不是方法。
总体测试结构
单元测试包含在规范
带一个的文件规范
每个实体(组件、页面、服务、管道等)的文件。的规范
文件与它们正在测试的源代码并排存在,并以其命名。例如,如果项目有一个名为WeatherService的服务,则该服务的代码位于一个名为weather.service.ts
在一个名为weather.service.spec.ts
.这两个文件都在同一个文件夹里。
的规范
文件本身包含一个描述
调用,它定义了整个测试。嵌套在它里面的是其他描述
定义主要功能区域的调用。每一个描述
调用可以包含设置和拆卸代码(通常通过beforeEach
而且afterEach
调用),更描述
调用形成功能的分层分解,以及它
定义单个测试用例的调用。
的描述
而且它
调用还包含描述性文本标签。在格式良好的测试中,描述
而且它
调用与它们的标签结合在一起,为每个测试用例执行适当的短语和完整的标签,通过组合描述
而且它
标签,创造一个完整的句子。
例如:
描述(“计算”,()= >{
描述(“分”,()= >{
它(“正确计算4 / 2”()= >{});
它(“懦夫拒绝除零”()= >{});
...
});
描述(“乘”,()= >{
...
});
});
外描述
调用声明计算
服务正在被测试,内部描述
调用准确地声明正在测试的功能,而它
调用声明测试用例是什么。当运行时,每个测试用例的完整标签都是一个有意义的句子(计算除法懦夫拒绝除零)。
页面和组件
页面只是Angular的组件。因此,页面和组件都使用Angular的组件测试指导方针。
由于页面和组件同时包含TypeScript代码和HTML模板标记,因此可以同时执行组件类测试和组件DOM测试。当创建一个页面时,生成的模板测试看起来像这样:
进口{CUSTOM_ELEMENTS_SCHEMA}从“@angular /核心”;
进口{异步,ComponentFixture,试验台}从“@angular /核心/测试”;
进口{TabsPage}从”。/ tabs.page ';
描述(“TabsPage”,()= >{
让组件:TabsPage;
让夹具:ComponentFixture<TabsPage>;
beforeEach(异步()= >{
试验台.configureTestingModule({
声明:[TabsPage],
模式:[CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});
beforeEach(()= >{
夹具=试验台.createComponent(TabsPage);
组件=夹具.componentInstance;
夹具.detectChanges();
});
它(“应该创建”,()= >{
预计(组件).toBeTruthy();
});
});
在进行组件类测试时,使用via定义的组件对象访问组件对象component = fixture.componentInstance;
.这是组件类的一个实例。在进行DOM测试时,fixture.nativeElement
属性被使用。这是实际的HTMLElement
该组件允许测试使用标准的HTML API方法,例如HTMLElement.querySelector
以便检查DOM。
服务
服务通常分为两大类:执行计算和其他操作的实用服务,以及主要执行HTTP操作和数据操作的数据服务。
基本服务测试
测试大多数服务的建议方法是实例化服务,并为该服务具有的任何依赖项手动注入模拟。这样,代码就可以单独测试。
假设有一个服务,该服务具有一种方法,它接受一组工卡并计算净工资。我们还假设税收计算是通过当前服务所依赖的另一个服务处理的。这个工资单服务可以这样测试:
进口{PayrollService}从”。/ payroll.service ';
描述(“PayrollService”,()= >{
让服务:PayrollService;
让taxServiceSpy;
beforeEach(()= >{
taxServiceSpy=茉莉花.createSpyObj(“TaxService”,{
federalIncomeTax:0,
stateIncomeTax:0,
社会保障:0,
医疗保险:0
});
服务=新PayrollService(taxServiceSpy);
});
描述(“净薪酬计算”,()= >{
...
});
});
这允许测试通过模拟设置控制各种税收计算返回的值,例如taxServiceSpy.federalIncomeTax.and.returnValue (73.24)
.这使得“净支付”测试独立于税收计算逻辑。当税务代码更改时,只需要更改与税务服务相关的代码和测试。净收入测试可以继续按原样运行,因为这些测试并不关心税收是如何计算的,只关心价值是否被正确应用。
通过生成服务时使用的脚手架betway东盟体育appIonic g服务名称
使用Angular的测试工具,并建立一个测试模块。这样做并不是严格必要的。然而,该代码可以保留,允许手动构建或注入服务:
进口{试验台,注入}从“@angular /核心/测试”;
进口{PayrollService}从”。/ payroll.service ';
进口{TaxService}从”。/ tax.service ';
描述(“PayrolService”,()= >{
让taxServiceSpy;
beforeEach(()= >{
taxServiceSpy=茉莉花.createSpyObj(“TaxService”,{
federalIncomeTax:0,
stateIncomeTax:0,
社会保障:0,
医疗保险:0,
});
试验台.configureTestingModule({
供应商:[PayrollService,{提供:TaxService,useValue:taxServiceSpy}],
});
});
它(“在注射的地方做一些测试”,注入([PayrollService],(服务:PayrollService)= >{
预计(服务).toBeTruthy();
}));
它(“在手动构建的地方进行一些测试”,()= >{
常量服务=新PayrollService(taxServiceSpy);
预计(服务).toBeTruthy();
});
});
测试HTTP数据服务
大多数执行HTTP操作的服务都会使用Angular的HttpClient服务来执行这些操作。对于这样的测试,建议使用Angular的HttpClientTestingModule
.有关这个模块的详细文档,请参阅Angular的文档Angular的HTTP请求测试指南。
这样一个测试的基本设置如下:
进口{HttpBackend,HttpClient}从“@angular /共同/ http”;
进口{HttpTestingController,HttpClientTestingModule}从“@angular /共同/ http /测试”;
进口{试验台,注入}从“@angular /核心/测试”;
进口{IssTrackingDataService}从”。/ iss-tracking-data.service ';
描述(“IssTrackingDataService”,()= >{
让httpClient:HttpClient;
让httpTestingController:HttpTestingController;
让issTrackingDataService:IssTrackingDataService;
beforeEach(()= >{
试验台.configureTestingModule({
进口:[HttpClientTestingModule],
供应商:[IssTrackingDataService],
});
httpClient=试验台.得到(HttpClient);
httpTestingController=试验台.得到(HttpTestingController);
issTrackingDataService=新IssTrackingDataService(httpClient);
});
它(“存在”,注入([IssTrackingDataService],(服务:IssTrackingDataService)= >{
预计(服务).toBeTruthy();
}));
描述(“位置”,()= >{
它(“现在知道国际空间站的位置了”,()= >{
issTrackingDataService.位置().订阅((x)= >{
预计(x).toEqual({经度:-138.1719,纬度:44.4423});
});
常量要求的事情=httpTestingController.expectOne(“http://api.open-notify.org/iss-now.json”);
预计(要求的事情.请求.方法).toEqual(“得到”);
要求的事情.冲洗({
iss_position:{经度:“-138.1719”,纬度:“44.4423”},
时间戳:1525950644,
消息:“成功”,
});
httpTestingController.验证();
});
});
});
管道
管道类似于具有特定定义接口的服务。它是一个包含一个公共方法的类,变换
,它操作输入值(和其他可选参数),以创建在页面上呈现的输出。要测试管道:实例化管道,调用transform方法,并验证结果。
作为一个简单的例子,让我们看看一个以a为参数的管道人
对象并格式化名称。为了简单起见,我们设a人
由id
,firstName
,姓
,middleInitial
.管道的需求是将名称打印为“Last, First M.”,以处理名、姓或中间首字母不存在的情况。这样的测试可能是这样的:
进口{NamePipe}从”。/ name.pipe ';
进口{人}从“. . / . . /模型/人”;
描述(“NamePipe”,()= >{
让管:NamePipe;
让testPerson:人;
beforeEach(()= >{
管=新NamePipe();
testPerson={
id:42,
firstName:“道格拉斯。”,
姓:“亚当斯”,
middleInitial:“N”,
};
});
它(“存在”,()= >{
预计(管).toBeTruthy();
});
它('正确格式化全名',()= >{
预计(管.变换(testPerson)).toBeEqual(亚当斯,道格拉斯·N。);
});
它(“句柄没有中间首字母”,()= >{
删除testPerson.middleInitial;
预计(管.变换(testPerson)).toBeEqual(“亚当斯,道格拉斯。”);
});
它(没有名字的句柄,()= >{
删除testPerson.firstName;
预计(管.变换(testPerson)).toBeEqual(“亚当斯N。”);
});
它(“没有姓氏的句柄”,()= >{
删除testPerson.姓;
预计(管.变换(testPerson)).toBeEqual(“道格拉斯N。”);
});
});
通过在使用管道的组件和页面中进行DOM测试来测试管道也是有益的。
端到端测试
端到端测试用于验证应用程序作为一个整体工作,并且通常包括到活动数据的连接。单元测试关注于隔离的代码单元,因此允许对应用程序逻辑进行低级测试,而端到端测试关注于各种用户故事或使用场景,提供对整个应用程序数据流的高级测试。单元测试试图发现应用程序逻辑中的问题,而端到端测试试图发现当这些单独的单元一起使用时发生的问题。端到端测试揭示了应用程序整体架构中的问题。
由于端到端测试使用用户描述,并将应用程序作为一个整体而不是单个代码模块覆盖,因此端到端测试存在于项目中与主应用程序本身的代码不同的应用程序中。大多数端到端测试通过自动化用户与应用程序的交互并检查DOM以确定这些交互的结果来进行操作。
测试结构
当一个@betway东盟体育appionic /角
应用程序时,将生成默认的端到端测试应用程序e2e
文件夹中。此应用程序使用量角器来控制浏览器和茉莉花来组织和执行测试。应用程序最初由四个文件组成:
protractor.conf.js
-量角器配置文件tsconfig.e2e.json
-测试应用程序的特定TypeScript配置src / app.po.ts
-一个页面对象,包含导航应用程序、查询DOM中的元素和操作页面上元素的方法src / app.e2e-spec.ts
-一个测试脚本
页面对象
端到端测试通过自动化用户与应用程序的交互、等待应用程序响应和检查DOM以确定交互的结果来进行操作。这涉及到大量DOM操作和检查。如果这些都是手动完成的,那么测试将非常脆弱,难以读取和维护。
Page对象在TypeScript类中封装了单个页面的HTML,提供了测试脚本用来与应用程序交互的API。在页面对象中封装DOM操作逻辑使测试更具可读性,更容易进行推理,从而降低了测试的维护成本。创建精心制作的页面对象是创建高质量和可维护的端到端测试的关键。
基页对象
许多测试依赖于诸如等待页面可见、在输入中输入文本以及单击按钮等操作。用于完成此操作的方法仅与用于获得适当的DOM元素更改的CSS选择器保持一致。因此,将此逻辑抽象为可由其他页面对象使用的基类是有意义的。
下面是一个示例,它实现了一些所有页面对象都需要支持的基本方法。
进口{浏览器,通过,元素,ExpectedConditions}从“量角器”;
出口类PageObjectBase{
私人路径:字符串;
受保护的标签:字符串;
构造函数(标签:字符串,路径:字符串){
这.标签=标签;
这.路径=路径;
}
负载(){
返回浏览器.得到(这.路径);
}
rootElement(){
返回元素(通过.css(这.标签));
}
waitUntilInvisible(){
浏览器.等待(ExpectedConditions.invisibilityOf(这.rootElement()),3000);
}
waitUntilPresent(){
浏览器.等待(ExpectedConditions.presenceOf(这.rootElement()),3000);
}
waitUntilNotPresent(){
浏览器.等待(ExpectedConditions.不(ExpectedConditions.presenceOf(这.rootElement())),3000);
}
waitUntilVisible(){
浏览器.等待(ExpectedConditions.visibilityOf(这.rootElement()),3000);
}
getTitle(){
返回元素(通过.css(`$ {这.标签}ion-title`)).getText();
}
受保护的enterInputText(选取:字符串,文本:字符串){
常量埃尔=元素(通过.css(`$ {这.标签}$ {选取}`));
常量可使=埃尔.元素(通过.css(“输入”));
可使.sendKeys(文本);
}
受保护的enterTextareaText(选取:字符串,文本:字符串){
常量埃尔=元素(通过.css(`$ {这.标签}$ {选取}`));
常量可使=埃尔.元素(通过.css(“文本区域”));
可使.sendKeys(文本);
}
受保护的clickButton(选取:字符串){
常量埃尔=元素(通过.css(`$ {这.标签}$ {选取}`));
浏览器.等待(ExpectedConditions.elementToBeClickable(埃尔));
埃尔.点击();
}
}
页面的抽象
应用程序中的每个页面都有自己的页面对象类,该类抽象了该页上的元素。如果使用基页对象类,则创建页对象主要涉及为特定于该页的元素创建自定义方法。通常,这些自定义元素利用基类中的方法来执行所需的工作。
下面是一个简单但典型的登录页面的示例页面对象。注意,许多方法,例如enterEMail ()
,调用基类中执行大部分工作的方法。
进口{浏览器,通过,元素,ExpectedConditions}从“量角器”;
进口{PageObjectBase}从”。/ base.po ';
出口类LoginPage扩展PageObjectBase{
构造函数(){
超级(“app-login”,/登录的);
}
waitForError(){
浏览器.等待(ExpectedConditions.presenceOf(元素(通过.css(' . error '))),3000);
}
getErrorMessage(){
返回元素(通过.css(' . error ')).getText();
}
enterEMail(电子邮件:字符串){
这.enterInputText(“# email-input”,电子邮件);
}
enterPassword(密码:字符串){
这.enterInputText(“#密码输入”,密码);
}
clickSignIn(){
这.clickButton(“# signin-button”);
}
}
测试脚本
与单元测试类似,端到端测试脚本由嵌套组成描述()
而且它()
功能。对于端到端测试,描述()
函数通常表示特定的场景它()
函数表示在该场景中执行操作时应用程序应该显示的特定行为。
中使用的标签也类似于单元测试描述()
而且它()
函数在使用“describe”或“it”以及连接在一起形成完整的测试用例时都应该是有意义的。
下面是一个示例端到端测试脚本,用于测试一些典型的登录场景。
进口{AppPage}从“. . /页面对象/页面/ app.po”;
进口{AboutPage}从“. . /页面对象/页面/ about.po”;
进口{CustomersPage}从“. . /页面对象/页面/ customers.po”;
进口{LoginPage}从“. . /页面对象/页面/ login.po”;
进口{MenuPage}从“. . /页面对象/页面/ menu.po”;
进口{TasksPage}从“. . /页面对象/页面/ tasks.po”;
描述(“登录”,()= >{
常量关于=新AboutPage();
常量应用程序=新AppPage();
常量客户=新CustomersPage();
常量登录=新LoginPage();
常量菜单=新MenuPage();
常量任务=新TasksPage();
beforeEach(()= >{
应用程序.负载();
});
描述(“登录前”,()= >{
它(“显示登录屏幕”,()= >{
预计(登录.rootElement().isDisplayed()).toEqual(真正的);
});
它(“允许应用内导航about”,()= >{
菜单.clickAbout();
关于.waitUntilVisible();
登录.waitUntilInvisible();
});
它(“不允许在应用程序内导航任务”,()= >{
菜单.clickTasks();
应用程序.waitForPageNavigation();
预计(登录.rootElement().isDisplayed()).toEqual(真正的);
});
它(“不允许用户在应用程序内导航”,()= >{
菜单.clickCustomers();
应用程序.waitForPageNavigation();
预计(登录.rootElement().isDisplayed()).toEqual(真正的);
});
它('如果登录失败显示错误消息',()= >{
登录.enterEMail(“test@test.com”);
登录.enterPassword(“假的”);
登录.clickSignIn();
登录.waitForError();
预计(登录.getErrorMessage()).toEqual('密码无效或用户没有密码。');
});
它('如果登录成功,将导航到任务页面',()= >{
登录.enterEMail(“test@test.com”);
登录.enterPassword(“测试”);
登录.clickSignIn();
任务.waitUntilVisible();
});
});
描述(“一旦登录”,()= >{
beforeEach(()= >{
任务.waitUntilVisible();
});
它(“允许导航到客户页面”,()= >{
菜单.clickCustomers();
客户.waitUntilVisible();
任务.waitUntilInvisible();
});
它(“允许导航到关于页面”,()= >{
菜单.clickAbout();
关于.waitUntilVisible();
任务.waitUntilInvisible();
});
它(“允许导航回任务页面”,()= >{
菜单.clickAbout();
任务.waitUntilInvisible();
菜单.clickTasks();
任务.waitUntilVisible();
});
});
});
配置
默认配置使用相同的方法environment.ts
用于开发的文件。为了更好地控制端到端测试所使用的数据,为测试创建一个特定的环境并将该环境用于测试通常是有用的。本节将展示创建此配置的一种可能方法。
测试环境
设置测试环境涉及创建一个新的环境文件,该文件使用专用的测试后端,更新angular.json
文件来使用该环境,并修改e2e
脚本。package.json
要指定测验
环境。
创建environment.e2e.ts
文件
角environment.ts
而且environment.prod.ts
文件通常用于存储诸如应用程序后端数据服务的基本URL之类的信息。创建一个environment.e2e.ts
这提供了相同的信息,只是连接到专门用于测试的后端服务,而不是开发或生产后端服务。这里有一个例子:
出口常量环境={
生产:假,
databaseURL:“https://e2e-test-api.my-great-app.com”,
projectId:“my-great-app-e2e”,
};
修改angular.json
文件
的angular.json
文件需要修改才能使用此文件。这是一个分层的过程。按照下面列出的xpath添加所需的配置。
在/项目/应用程序/建筑师/构建/配置
被称为测验
执行文件替换:
“测试”:{
“fileReplacements”:[
{
“替换”:“src /环境/ environment.ts”,
”与“:“src /环境/ environment.e2e.ts”
}
]
}
在/项目/ app /建筑师/服务/配置
被称为测验
将浏览器目标指向测验
构建上面定义的配置。
“测试”:{
“browserTarget”:“应用程序:构建:测试”
}
在/项目/ app-e2e /建筑师/ e2e /配置
被称为测验
将开发服务器目标指向测验
服务上面定义的配置。
“测试”:{
“devServerTarget”:“应用程序服务:测试”
}
修改package.json
文件
修改package.json
存档以便NPM运行e2e
使用测验
配置。
“脚本”:{
“e2e”:"ng e2e——configuration=test",
“棉絮”:“ng线头”,
“ng”:“ng”,
“开始”:“ng服务”,
“测试”:“ng测试”,
“测试:开发”:"ng test——浏览器=ChromeHeadlessCI",
“测试:ci”:"ng test——no-watch——浏览器=ChromeHeadlessCI"
},
测试清理
如果端到端测试以任何方式修改数据,那么在测试完成后将数据重置为已知状态是有帮助的。一种方法是:
- 创建执行清理的端点。
- 添加一个
onCleanUp ()
函数配置
对象导出的protractor.conf.js
文件。
这里有一个例子:
onCleanUp(){
常量axios=需要(“axios”);
返回axios
.帖子(
“https://e2e-test-api.my-great-app.com/purgeDatabase”,
{}
)
.然后(res= >{
控制台.日志(res.数据);
})
.抓(犯错= >控制台.日志(犯错));
}