Ionic测试策略 #
一、测试概述 #
1.1 测试金字塔 #
text
/\
/ \
/ E2E\ 端到端测试
/------\ 数量少、成本高
/ \
/ Integration\ 集成测试
/--------------\ 数量中等
/ \
/ Unit Tests \ 单元测试
-------------------- 数量多、成本低
1.2 测试工具 #
| 工具 | 用途 |
|---|---|
| Jest | 单元测试 |
| Jasmine | 单元测试 |
| Karma | 测试运行器 |
| Cypress | E2E测试 |
| Protractor | E2E测试 |
二、单元测试 #
2.1 Jest配置 #
bash
npm install jest @types/jest jest-preset-angular
javascript
// jest.config.js
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/e2e/'],
moduleNameMapper: {
'@app/(.*)': '<rootDir>/src/app/$1'
}
};
typescript
// setup-jest.ts
import 'jest-preset-angular/setup-jest';
2.2 服务测试 #
typescript
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should get users', () => {
const mockUsers = [
{ id: '1', name: 'User 1' },
{ id: '2', name: 'User 2' }
];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
2.3 组件测试 #
typescript
// home.component.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HomePage],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display title', () => {
component.title = 'Test Title';
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('ion-title');
expect(element.textContent).toContain('Test Title');
});
});
2.4 管道测试 #
typescript
// filter.pipe.spec.ts
import { FilterPipe } from './filter.pipe';
describe('FilterPipe', () => {
let pipe: FilterPipe;
beforeEach(() => {
pipe = new FilterPipe();
});
it('should filter items', () => {
const items = [
{ name: 'Apple' },
{ name: 'Banana' },
{ name: 'Apricot' }
];
const result = pipe.transform(items, 'Ap');
expect(result.length).toBe(2);
expect(result[0].name).toBe('Apple');
expect(result[1].name).toBe('Apricot');
});
it('should return empty array for no match', () => {
const items = [
{ name: 'Apple' },
{ name: 'Banana' }
];
const result = pipe.transform(items, 'Orange');
expect(result.length).toBe(0);
});
});
三、Ionic组件测试 #
3.1 测试Ionic组件 #
typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IonicModule, AlertController } from '@ionic/angular';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let alertCtrl: AlertController;
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [MyComponent],
imports: [IonicModule.forRoot()],
providers: [
AlertController
]
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
alertCtrl = TestBed.inject(AlertController);
});
it('should show alert', async () => {
spyOn(alertCtrl, 'create').and.returnValue(Promise.resolve({
present: () => Promise.resolve()
} as any));
await component.showAlert();
expect(alertCtrl.create).toHaveBeenCalled();
});
});
3.2 测试导航 #
typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
let router: Router;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [HomePage],
imports: [IonicModule.forRoot()],
providers: [
{ provide: Router, useValue: { navigate: jasmine.createSpy('navigate') } }
]
});
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
router = TestBed.inject(Router);
});
it('should navigate to detail', () => {
component.goToDetail('123');
expect(router.navigate).toHaveBeenCalledWith(['/detail', '123']);
});
});
四、E2E测试 #
4.1 Cypress安装 #
bash
npm install cypress @cypress/angular
npx cypress open
4.2 Cypress配置 #
javascript
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8100',
supportFile: 'cypress/support/e2e.ts',
specPattern: 'cypress/e2e/**/*.cy.ts'
}
});
4.3 登录测试 #
typescript
// cypress/e2e/login.cy.ts
describe('Login', () => {
beforeEach(() => {
cy.visit('/login');
});
it('should display login form', () => {
cy.get('ion-input[formControlName="email"]').should('exist');
cy.get('ion-input[formControlName="password"]').should('exist');
cy.get('ion-button[type="submit"]').should('exist');
});
it('should login successfully', () => {
cy.get('ion-input[formControlName="email"] input').type('user@example.com');
cy.get('ion-input[formControlName="password"] input').type('password123');
cy.get('ion-button[type="submit"]').click();
cy.url().should('include', '/home');
});
it('should show error for invalid credentials', () => {
cy.get('ion-input[formControlName="email"] input').type('wrong@example.com');
cy.get('ion-input[formControlName="password"] input').type('wrongpassword');
cy.get('ion-button[type="submit"]').click();
cy.get('ion-toast').should('exist');
});
});
4.4 列表测试 #
typescript
// cypress/e2e/users.cy.ts
describe('Users List', () => {
beforeEach(() => {
cy.login();
cy.visit('/users');
});
it('should display users list', () => {
cy.get('ion-item').should('have.length.gt', 0);
});
it('should navigate to user detail', () => {
cy.get('ion-item').first().click();
cy.url().should('include', '/users/');
});
it('should search users', () => {
cy.get('ion-searchbar input').type('John');
cy.get('ion-item').each($item => {
cy.wrap($item).should('contain', 'John');
});
});
});
4.5 自定义命令 #
typescript
// cypress/support/e2e.ts
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email: 'test@example.com',
password: 'password123'
}
}).then(response => {
window.localStorage.setItem('token', response.body.token);
});
});
// 使用
cy.login();
五、测试覆盖率 #
5.1 Jest覆盖率 #
javascript
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['html', 'lcov', 'text'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
5.2 运行覆盖率 #
bash
npm run test -- --coverage
六、测试最佳实践 #
6.1 AAA模式 #
typescript
it('should calculate total', () => {
// Arrange
const items = [
{ price: 10, quantity: 2 },
{ price: 20, quantity: 1 }
];
// Act
const total = service.calculateTotal(items);
// Assert
expect(total).toBe(40);
});
6.2 测试隔离 #
typescript
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
// 每个测试用例前重新创建服务
TestBed.configureTestingModule({});
service = TestBed.inject(UserService);
});
afterEach(() => {
// 每个测试用例后清理
// ...
});
});
6.3 Mock外部依赖 #
typescript
describe('MyComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: ExternalService,
useValue: {
getData: jasmine.createSpy('getData').and.returnValue(of(mockData))
}
}
]
});
});
});
七、持续集成 #
7.1 GitHub Actions #
yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test -- --no-watch --browsers=ChromeHeadless
- name: Run E2E tests
run: npm run e2e
八、总结 #
8.1 测试要点 #
| 类型 | 工具 | 用途 |
|---|---|---|
| 单元测试 | Jest | 测试函数和服务 |
| 组件测试 | Jest + TestBed | 测试组件 |
| E2E测试 | Cypress | 测试用户流程 |
| 覆盖率 | Jest | 衡量测试完整性 |
8.2 下一步 #
掌握了测试策略后,接下来让我们学习 部署发布,了解Ionic应用的部署流程!
最后更新:2026-03-28