33、Angular应用测试全解析

Angular应用测试全解析

1. 单元测试基础

在Angular应用的单元测试中,有几个关键要点需要注意。首先,在测试HTTP请求时, verify() 方法可以用来测试所有的HTTP请求,并且在每个测试用例运行后的清理阶段,我们可以断言没有未完成的请求。同时,每个测试用例的代码通常会被包裹在 async() 函数中,这能确保 expect() 调用在所有异步调用完成后执行。

对于使用路由的组件测试,Angular提供了 RouterTestingModule ,它可以拦截导航但不加载目标组件。测试时,我们可以使用应用中相同的路由配置,也可以为测试创建单独的配置。用户可以通过与应用交互或直接在浏览器地址栏输入URL来导航应用, Router 对象负责应用代码中的导航, Location 对象代表地址栏中的URL,二者协同工作。

为了测试路由是否能正确导航应用,我们可以在测试用例中调用 navigate() navigateByUrl() 方法,并根据需要传递参数。例如,下面是一个路由配置的示例:

export const routes: Routes = [
  {path: '', component: HomeComponent},
  {path: 'product/:id', component: ProductDetailComponent}
];

当用户点击“Product Details”链接时,应用会导航到 ProductDetailComponent 。以下是相关的组件代码:

@Component({
  selector: 'app-root',
  template: `
    <a [routerLink]="['/']">Home</a>
    <a id="product" [routerLink]="['/product', productId]">
      Product Detail</a>
    <router-outlet></router-outlet>
  `
})
export class AppComponent {
  productId = 1234;
}

在测试中,我们可以通过以下步骤来验证点击链接后URL是否正确:
1. 使用 TestBed.get() API注入 Router Location 对象。
2. 使用 By.css() API获取对应的DOM对象,模拟点击链接。
3. 使用 triggerEventHandler() 方法模拟点击事件。
4. 使用 fakeAsync() 函数包裹导航代码, tick() 函数确保异步导航完成后再进行断言。

以下是具体的测试代码:

// imports omitted for brevity
describe('AppComponent', () => {
  let fixture;
  let router: Router;
  let location: Location;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule.withRoutes(routes)],
      declarations: [
        AppComponent, ProductDetailComponent, HomeComponent
      ]
    }).compileComponents();
  }));
  beforeEach(fakeAsync(() => {
    router = TestBed.get(Router);
    location = TestBed.get(Location);
    fixture = TestBed.createComponent(AppComponent);
    router.navigateByUrl('/');
    tick();
    fixture.detectChanges();
  }));
  it('can navigate and pass params to the product detail view',
    fakeAsync(() => {
      const productLink = fixture.debugElement.query(By.css('#product'));
      productLink.triggerEventHandler('click', {button: 0});
      tick();
      fixture.detectChanges();
      expect(location.path()).toEqual('/product/1234');
    }));
});

下面是测试导航的步骤流程图:

graph LR
    A[Configure test module] --> B[Inject router and location]
    B --> C[Instantiate AppComponent]
    C --> D[Click on the link]
    D --> E[Locate the link]
    E --> F[Product Detail]
    F --> G[Update the UI of AppComponent]
    G --> H[Update the UI of ProductDetailComponent]
    H --> I[Assert that URL contains /product/1234]
2. 端到端测试概述

单元测试可以确保Angular应用的每个独立部分按预期工作,但如何确保多个组件、服务和其他部分协同工作正常,而无需手动测试每个工作流程呢?这就需要端到端(E2E)测试。

E2E测试通过模拟用户与应用的交互来测试整个应用的工作流程。例如,下单过程可能涉及多个组件和服务,我们可以创建一个E2E测试来确保这个工作流程按预期进行。与单元测试不同,E2E测试使用真实的依赖项。

Protractor是一个测试库,它允许我们模拟用户操作来测试应用工作流程,而无需手动执行。默认情况下,Protractor使用Jasmine语法进行测试。它基于Selenium WebDriver,可以自动驱动浏览器。

当使用Angular CLI生成新项目时,会包含Protractor及其配置文件,以及 e2e 目录和示例测试脚本。运行E2E测试时,我们可以使用 ng e2e 命令,它会根据 protractor.conf.js 文件的配置加载测试脚本。以下是 protractor.conf.js 文件的部分配置示例:

specs: [
  './e2e/**/*.e2e-spec.ts'
],
capabilities: {
  'browserName': 'chrome'
},
directConnect: true

Protractor提供了一些API来与所有支持的框架一起工作:
- browser :提供控制浏览器的API,如 getCurrentUrl() wait() 等。
- by :用于通过ID、CSS、按钮或链接文本等查找Angular应用中的元素。
- element :用于查找和处理网页上的单个元素。
- element.all :用于查找和处理元素集合,如遍历HTML列表或表格的元素。

我们可以使用以下两种方法编写E2E测试:
1. 在同一脚本中,使用元素的ID或CSS类定位DOM元素,并断言应用逻辑是否正确。但这种方法的ID或CSS类可能会随时间变化,需要更新多个脚本。
2. 实现Page Object设计模式,将期望和断言写在一个文件中,将与UI元素交互和调用应用API的代码写在另一个文件中。这种方法可以减少代码重复,提高代码的可维护性。

3. Angular CLI生成的E2E测试

当使用Angular CLI生成新项目时,会在 e2e 目录下创建三个文件:
- app.po.ts AppComponent 的页面对象。
- app.e2e-spec.ts :生成的 AppComponent 的E2E测试。
- tsconfig.e2e.json :TypeScript编译器选项。

app.po.ts 文件包含一个简单的 AppPage 类,有两个方法:

import {browser, by, element} from 'protractor';
export class AppPage {
  navigateTo() {
    return browser.get('/');
  }
  getParagraphText() {
    return element(by.css('app-root h1')).getText();
  }
}

app.e2e-spec.ts 文件的测试代码如下:

import {AppPage} from './app.po';
describe('e2e-testing-samples App', () => {
  let page: AppPage;
  beforeEach(() => {
    page = new AppPage();
  });
  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toEqual('Welcome to app!');
  });
});

这个测试代码很容易理解,通过导航到着陆页并获取段落内容进行断言。我们可以使用 ng e2e 命令运行这个E2E测试。

以下是相关文件及其功能的表格:
| 文件名称 | 功能描述 |
| ---- | ---- |
| app.po.ts | 包含页面对象类和相关方法,用于定位元素和执行操作 |
| app.e2e-spec.ts | 包含测试用例,使用页面对象的API进行测试断言 |
| tsconfig.e2e.json | TypeScript编译器选项,用于E2E测试 |

Angular应用测试全解析

4. 测试登录页面

接下来,我们将以一个包含登录页面和主页的简单应用为例,详细介绍如何编写E2E测试。该应用的路由配置如下:

[{path: '', redirectTo: 'login', pathMatch: 'full'},
{path: 'login', component: LoginComponent},
{path: 'home', component: HomeComponent}]

HomeComponent 的模板只有一行:

<h1>Home Component</h1>

LoginComponent 包含一个登录按钮和一个表单,用于输入ID和密码。如果用户输入的ID为 Joe ,密码为 password ,应用将导航到主页;否则,将停留在登录页面并显示错误消息“Invalid ID or password”。以下是 LoginComponent 的代码:

@Component({
  selector: 'app-home',
  template: `<h1 class="home">Login Component</h1>
    <form #f="ngForm" (ngSubmit)="login(f.value)">
      ID: <input name="id" ngModel/><br>
      PWD: <input type="password" name="pwd" ngModel=""/><br>
      <button type="submit">Login</button>
      <span id="errMessage"
        *ngIf="wrongCredentials">Invalid ID or password</span>
    </form>
  `
})
export class LoginComponent {
  wrongCredentials = false;
  constructor(private router: Router) {}
  login(formValue) {
    if ('Joe' === formValue.id && 'password' === formValue.pwd) {
      this.router.navigate(['/home']);
      this.wrongCredentials = false;
    } else {
      this.router.navigate(['/login']);
      this.wrongCredentials = true;
    }
  }
}

测试代码位于 e2e 目录下,包含两个页面对象文件 login.po.ts home.po.ts ,以及一个测试脚本 login.e2e-spec.ts

home.po.ts 文件包含一个方法,用于返回页面标题的文本:

import {by, element} from 'protractor';
export class HomePage {
  getHeaderText() {
    return element(by.css('h1')).getText();
  }
}

login.po.ts 文件使用定位器获取表单字段和按钮的引用,并提供了模拟用户登录操作的方法:

import {browser, by, element, $} from 'protractor';
export class LoginPage {
  id = $('input[name="id"]');
  pwd = $('input[name="pwd"]');
  submit = element(by.buttonText('Login'));
  errMessage = element(by.id('errMessage'));
  login(id: string, password: string): void {
    this.id.sendKeys(id);
    this.pwd.sendKeys(password);
    this.submit.click();
  }
  navigateToLogin() {
    return browser.get('/login');
  }
  getErrorMessage() {
    return this.errMessage;
  }
}

login.e2e-spec.ts 文件包含测试套件,用于测试登录流程的成功和失败情况:

import {LoginPage} from './login.po';
import {HomePage} from './home.po';
import {browser} from 'protractor';
describe('Login page', () => {
  let loginPage: LoginPage;
  let homePage: HomePage;
  beforeEach(() => {
    loginPage = new LoginPage();
  });
  it('should navigate to login page and log in', () => {
    loginPage.navigateToLogin();
    loginPage.login('Joe', 'password');
    const url = browser.getCurrentUrl();
    expect(url).toContain('/home');
    homePage = new HomePage();
    expect(homePage.getHeaderText()).toEqual('Home Component');
  });
  it('should stay on login page if wrong credentials entered',
    () => {
      loginPage.navigateToLogin();
      loginPage.login('Joe', 'wrongpassword');
      const url = browser.getCurrentUrl();
      expect(url).toContain('/login');
      expect(loginPage.getErrorMessage().isPresent()).toBe(true);
    });
});

下面是登录测试的步骤流程图:

graph LR
    A[Instantiate LoginPage] --> B[Navigate to login page]
    B --> C{Enter credentials}
    C -- Correct --> D[Navigate to home page]
    D --> E[Assert URL contains /home]
    E --> F[Assert page header is correct]
    C -- Incorrect --> G[Stay on login page]
    G --> H[Assert URL contains /login]
    H --> I[Assert error message is shown]
5. 处理异步操作

在实际应用中,有些操作可能需要一些时间才能完成,例如登录时与认证服务器通信。在这种情况下,我们需要等待操作完成后再进行断言。Protractor提供了 browser.wait() 方法来处理这种情况。

例如,在点击搜索按钮后,应用会发起一个HTTP请求来获取产品信息,这个请求可能需要一些时间才能完成。我们可以使用一个辅助函数来等待URL变化后再进行断言。

在前面的登录测试中,如果登录过程需要与认证服务器通信,我们可以使用 browser.wait() 方法来确保在URL更新后再进行断言:

it('should navigate to login page and log in', () => {
  loginPage.navigateToLogin();
  loginPage.login('Joe', 'password');
  browser.wait(() => {
    return browser.getCurrentUrl().then(url => {
      return url.includes('/home');
    });
  }, 5000, 'URL did not change to /home within 5 seconds');
  const url = browser.getCurrentUrl();
  expect(url).toContain('/home');
  homePage = new HomePage();
  expect(homePage.getHeaderText()).toEqual('Home Component');
});
6. 总结

通过以上内容,我们详细介绍了Angular应用的单元测试和端到端测试。单元测试可以确保每个独立部分按预期工作,而端到端测试可以模拟用户操作,确保整个应用的工作流程正常。

在单元测试中,我们学习了如何测试HTTP请求、使用路由的组件,以及如何处理异步操作。在端到端测试中,我们了解了Protractor的使用方法,包括其API、配置文件和编写测试的两种方法。

为了提高测试的可维护性和可扩展性,我们推荐使用Page Object设计模式。同时,在处理异步操作时,要注意使用 browser.wait() 方法来确保断言在操作完成后进行。

以下是单元测试和端到端测试的对比表格:
| 测试类型 | 测试范围 | 依赖项 | 测试方法 | 适用场景 |
| ---- | ---- | ---- | ---- | ---- |
| 单元测试 | 单个组件、服务等 | 模拟依赖项 | 编写测试用例,使用 async() fakeAsync() 等函数处理异步操作 | 验证单个模块的功能 |
| 端到端测试 | 整个应用工作流程 | 真实依赖项 | 使用Protractor模拟用户操作,使用Page Object设计模式 | 验证多个模块协同工作的功能 |

希望这些内容能帮助你更好地进行Angular应用的测试工作。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值