testing-tauri-apps
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Tauri Applications
Tauri应用测试
This skill covers testing strategies for Tauri v2 applications: unit testing with mocks, end-to-end testing with WebDriver, and CI integration.
本技能涵盖Tauri v2应用的测试策略:使用Mock的单元测试、基于WebDriver的端到端测试,以及CI集成。
Testing Approaches Overview
测试方法概述
Tauri supports two primary testing methodologies:
- Unit/Integration Testing - Uses a mock runtime without executing native webview libraries
- End-to-End Testing - Uses WebDriver protocol for browser automation
Tauri支持两种主要的测试方法:
- 单元/集成测试 - 使用模拟运行时,无需执行原生WebView库
- 端到端测试 - 使用WebDriver协议实现浏览器自动化
Mocking Tauri APIs
Mock Tauri API
The module simulates a Tauri environment during frontend testing.
@tauri-apps/api/mocks@tauri-apps/api/mocksInstall Mock Dependencies
安装Mock依赖
bash
npm install -D vitest @tauri-apps/apibash
npm install -D vitest @tauri-apps/apiMock IPC Commands
Mock IPC命令
javascript
import { mockIPC, clearMocks } from '@tauri-apps/api/mocks';
import { invoke } from '@tauri-apps/api/core';
import { vi, describe, it, expect, afterEach } from 'vitest';
afterEach(() => {
clearMocks();
});
describe('Tauri Commands', () => {
it('should mock the add command', async () => {
mockIPC((cmd, args) => {
if (cmd === 'add') {
return (args.a as number) + (args.b as number);
}
});
const result = await invoke('add', { a: 12, b: 15 });
expect(result).toBe(27);
});
it('should verify invoke was called', async () => {
mockIPC((cmd) => {
if (cmd === 'greet') return 'Hello!';
});
const spy = vi.spyOn(window.__TAURI_INTERNALS__, 'invoke');
await invoke('greet', { name: 'World' });
expect(spy).toHaveBeenCalled();
});
});javascript
import { mockIPC, clearMocks } from '@tauri-apps/api/mocks';
import { invoke } from '@tauri-apps/api/core';
import { vi, describe, it, expect, afterEach } from 'vitest';
afterEach(() => {
clearMocks();
});
describe('Tauri Commands', () => {
it('should mock the add command', async () => {
mockIPC((cmd, args) => {
if (cmd === 'add') {
return (args.a as number) + (args.b as number);
}
});
const result = await invoke('add', { a: 12, b: 15 });
expect(result).toBe(27);
});
it('should verify invoke was called', async () => {
mockIPC((cmd) => {
if (cmd === 'greet') return 'Hello!';
});
const spy = vi.spyOn(window.__TAURI_INTERNALS__, 'invoke');
await invoke('greet', { name: 'World' });
expect(spy).toHaveBeenCalled();
});
});Mock Sidecar and Shell Commands
Mock Sidecar与Shell命令
javascript
import { mockIPC } from '@tauri-apps/api/mocks';
mockIPC(async (cmd, args) => {
if (args.message.cmd === 'execute') {
const eventCallbackId = `_${args.message.onEventFn}`;
const eventEmitter = window[eventCallbackId];
eventEmitter({ event: 'Stdout', payload: 'process output data' });
eventEmitter({ event: 'Terminated', payload: { code: 0 } });
}
});javascript
import { mockIPC } from '@tauri-apps/api/mocks';
mockIPC(async (cmd, args) => {
if (args.message.cmd === 'execute') {
const eventCallbackId = `_${args.message.onEventFn}`;
const eventEmitter = window[eventCallbackId];
eventEmitter({ event: 'Stdout', payload: 'process output data' });
eventEmitter({ event: 'Terminated', payload: { code: 0 } });
}
});Mock Events (v2.7.0+)
Mock事件 (v2.7.0+)
javascript
import { mockIPC } from '@tauri-apps/api/mocks';
import { emit, listen } from '@tauri-apps/api/event';
mockIPC(() => {}, { shouldMockEvents: true });
const eventHandler = vi.fn();
await listen('test-event', eventHandler);
await emit('test-event', { foo: 'bar' });
expect(eventHandler).toHaveBeenCalled();javascript
import { mockIPC } from '@tauri-apps/api/mocks';
import { emit, listen } from '@tauri-apps/api/event';
mockIPC(() => {}, { shouldMockEvents: true });
const eventHandler = vi.fn();
await listen('test-event', eventHandler);
await emit('test-event', { foo: 'bar' });
expect(eventHandler).toHaveBeenCalled();Mock Windows
Mock窗口
javascript
import { mockWindows } from '@tauri-apps/api/mocks';
import { getCurrent, getAll } from '@tauri-apps/api/webviewWindow';
mockWindows('main', 'second', 'third');
// First parameter is the "current" window
expect(getCurrent()).toHaveProperty('label', 'main');
expect(getAll().map((w) => w.label)).toEqual(['main', 'second', 'third']);javascript
import { mockWindows } from '@tauri-apps/api/mocks';
import { getCurrent, getAll } from '@tauri-apps/api/webviewWindow';
mockWindows('main', 'second', 'third');
// 第一个参数是“当前”窗口
expect(getCurrent()).toHaveProperty('label', 'main');
expect(getAll().map((w) => w.label)).toEqual(['main', 'second', 'third']);Vitest Configuration
Vitest配置
javascript
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./test/setup.js'],
},
});
// test/setup.js
window.__TAURI_INTERNALS__ = {
invoke: vi.fn(),
transformCallback: vi.fn(),
};javascript
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./test/setup.js'],
},
});
// test/setup.js
window.__TAURI_INTERNALS__ = {
invoke: vi.fn(),
transformCallback: vi.fn(),
};WebDriver End-to-End Testing
WebDriver端到端测试
WebDriver testing uses to automate Tauri applications.
tauri-driverWebDriver测试使用实现Tauri应用的自动化。
tauri-driverPlatform Support
平台支持
| Platform | Support | Notes |
|---|---|---|
| Windows | Full | Requires Microsoft Edge Driver |
| Linux | Full | Requires WebKitWebDriver |
| macOS | None | WKWebView lacks WebDriver tooling |
| 平台 | 支持情况 | 说明 |
|---|---|---|
| Windows | 完全支持 | 需要Microsoft Edge Driver |
| Linux | 完全支持 | 需要WebKitWebDriver |
| macOS | 不支持 | WKWebView缺少WebDriver工具链 |
Install tauri-driver
安装tauri-driver
bash
cargo install tauri-driver --lockedbash
cargo install tauri-driver --lockedPlatform Dependencies
平台依赖
bash
undefinedbash
undefinedLinux (Debian/Ubuntu)
Linux (Debian/Ubuntu)
sudo apt install webkit2gtk-driver xvfb
which WebKitWebDriver # Verify installation
sudo apt install webkit2gtk-driver xvfb
which WebKitWebDriver # 验证安装
Windows (PowerShell)
Windows (PowerShell)
cargo install --git https://github.com/chippers/msedgedriver-tool
& "$HOME/.cargo/bin/msedgedriver-tool.exe"
undefinedcargo install --git https://github.com/chippers/msedgedriver-tool
& "$HOME/.cargo/bin/msedgedriver-tool.exe"
undefinedWebdriverIO Setup
WebdriverIO配置
Project Structure
项目结构
my-tauri-app/
├── src-tauri/
├── src/
└── e2e-tests/
├── package.json
├── wdio.conf.js
└── specs/
└── app.spec.jsmy-tauri-app/
├── src-tauri/
├── src/
└── e2e-tests/
├── package.json
├── wdio.conf.js
└── specs/
└── app.spec.jsPackage Configuration
包配置
json
{
"name": "tauri-e2e-tests",
"version": "1.0.0",
"type": "module",
"scripts": { "test": "wdio run wdio.conf.js" },
"dependencies": { "@wdio/cli": "^9.19.0" },
"devDependencies": {
"@wdio/local-runner": "^9.19.0",
"@wdio/mocha-framework": "^9.19.0",
"@wdio/spec-reporter": "^9.19.0"
}
}json
{
"name": "tauri-e2e-tests",
"version": "1.0.0",
"type": "module",
"scripts": { "test": "wdio run wdio.conf.js" },
"dependencies": { "@wdio/cli": "^9.19.0" },
"devDependencies": {
"@wdio/local-runner": "^9.19.0",
"@wdio/mocha-framework": "^9.19.0",
"@wdio/spec-reporter": "^9.19.0"
}
}WebdriverIO Configuration
WebdriverIO配置
javascript
// e2e-tests/wdio.conf.js
import { spawn, spawnSync } from 'child_process';
let tauriDriver;
export const config = {
hostname: '127.0.0.1',
port: 4444,
specs: ['./specs/**/*.js'],
maxInstances: 1,
capabilities: [{
browserName: 'wry',
'tauri:options': {
application: '../src-tauri/target/debug/my-tauri-app',
},
}],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: { ui: 'bdd', timeout: 60000 },
onPrepare: () => {
const result = spawnSync('cargo', ['build', '--manifest-path', '../src-tauri/Cargo.toml'], {
stdio: 'inherit',
});
if (result.status !== 0) throw new Error('Failed to build Tauri app');
},
beforeSession: () => {
tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'] });
return new Promise((resolve) => {
tauriDriver.stdout.on('data', (data) => {
if (data.toString().includes('listening')) resolve();
});
});
},
afterSession: () => tauriDriver?.kill(),
};javascript
// e2e-tests/wdio.conf.js
import { spawn, spawnSync } from 'child_process';
let tauriDriver;
export const config = {
hostname: '127.0.0.1',
port: 4444,
specs: ['./specs/**/*.js'],
maxInstances: 1,
capabilities: [{
browserName: 'wry',
'tauri:options': {
application: '../src-tauri/target/debug/my-tauri-app',
},
}],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: { ui: 'bdd', timeout: 60000 },
onPrepare: () => {
const result = spawnSync('cargo', ['build', '--manifest-path', '../src-tauri/Cargo.toml'], {
stdio: 'inherit',
});
if (result.status !== 0) throw new Error('Failed to build Tauri app');
},
beforeSession: () => {
tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'] });
return new Promise((resolve) => {
tauriDriver.stdout.on('data', (data) => {
if (data.toString().includes('listening')) resolve();
});
});
},
afterSession: () => tauriDriver?.kill(),
};WebdriverIO Test Example
WebdriverIO测试示例
javascript
// e2e-tests/specs/app.spec.js
describe('My Tauri App', () => {
it('should display the header', async () => {
const header = await $('body > h1');
expect(await header.getText()).toMatch(/^[hH]ello/);
});
it('should interact with a button', async () => {
const button = await $('#greet-button');
await button.click();
const output = await $('#greet-output');
await output.waitForExist({ timeout: 5000 });
expect(await output.getText()).toContain('Hello');
});
});javascript
// e2e-tests/specs/app.spec.js
describe('My Tauri App', () => {
it('should display the header', async () => {
const header = await $('body > h1');
expect(await header.getText()).toMatch(/^[hH]ello/);
});
it('should interact with a button', async () => {
const button = await $('#greet-button');
await button.click();
const output = await $('#greet-output');
await output.waitForExist({ timeout: 5000 });
expect(await output.getText()).toContain('Hello');
});
});Selenium Setup
Selenium配置
Package Configuration
包配置
json
{
"name": "tauri-selenium-tests",
"version": "1.0.0",
"scripts": { "test": "mocha" },
"dependencies": {
"chai": "^5.2.1",
"mocha": "^11.7.1",
"selenium-webdriver": "^4.34.0"
}
}json
{
"name": "tauri-selenium-tests",
"version": "1.0.0",
"scripts": { "test": "mocha" },
"dependencies": {
"chai": "^5.2.1",
"mocha": "^11.7.1",
"selenium-webdriver": "^4.34.0"
}
}Selenium Test Example
Selenium测试示例
javascript
// e2e-tests/test/test.js
import { spawn, spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import { Builder, By } from 'selenium-webdriver';
import { expect } from 'chai';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let driver, tauriDriver;
const application = path.resolve(__dirname, '../../src-tauri/target/debug/my-tauri-app');
describe('Tauri App Tests', function () {
this.timeout(60000);
before(async function () {
spawnSync('cargo', ['build', '--manifest-path', '../../src-tauri/Cargo.toml'], {
cwd: __dirname, stdio: 'inherit',
});
tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'] });
await new Promise((resolve) => {
tauriDriver.stdout.on('data', (data) => {
if (data.toString().includes('listening')) resolve();
});
});
driver = await new Builder()
.usingServer('http://127.0.0.1:4444/')
.withCapabilities({ browserName: 'wry', 'tauri:options': { application } })
.build();
});
after(async function () {
await driver?.quit();
tauriDriver?.kill();
});
it('should display greeting', async function () {
const header = await driver.findElement(By.css('body > h1'));
expect(await header.getText()).to.match(/^[hH]ello/);
});
it('should click button and show output', async function () {
const button = await driver.findElement(By.id('greet-button'));
await button.click();
const output = await driver.findElement(By.id('greet-output'));
expect(await output.getText()).to.include('Hello');
});
});javascript
// e2e-tests/test/test.js
import { spawn, spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
import { Builder, By } from 'selenium-webdriver';
import { expect } from 'chai';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let driver, tauriDriver;
const application = path.resolve(__dirname, '../../src-tauri/target/debug/my-tauri-app');
describe('Tauri App Tests', function () {
this.timeout(60000);
before(async function () {
spawnSync('cargo', ['build', '--manifest-path', '../../src-tauri/Cargo.toml'], {
cwd: __dirname, stdio: 'inherit',
});
tauriDriver = spawn('tauri-driver', [], { stdio: ['ignore', 'pipe', 'pipe'] });
await new Promise((resolve) => {
tauriDriver.stdout.on('data', (data) => {
if (data.toString().includes('listening')) resolve();
});
});
driver = await new Builder()
.usingServer('http://127.0.0.1:4444/')
.withCapabilities({ browserName: 'wry', 'tauri:options': { application } })
.build();
});
after(async function () {
await driver?.quit();
tauriDriver?.kill();
});
it('should display greeting', async function () {
const header = await driver.findElement(By.css('body > h1'));
expect(await header.getText()).to.match(/^[hH]ello/);
});
it('should click button and show output', async function () {
const button = await driver.findElement(By.id('greet-button'));
await button.click();
const output = await driver.findElement(By.id('greet-output'));
expect(await output.getText()).to.include('Hello');
});
});CI Integration with GitHub Actions
与GitHub Actions的CI集成
yaml
undefinedyaml
undefined.github/workflows/e2e-tests.yml
.github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential \
curl wget file libxdo-dev libssl-dev \
libayatana-appindicator3-dev librsvg2-dev \
webkit2gtk-driver xvfb
- uses: dtolnay/rust-action@stable
- run: cargo install tauri-driver --locked
- name: Setup Windows WebDriver
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
cargo install --git https://github.com/chippers/msedgedriver-tool
& "$HOME/.cargo/bin/msedgedriver-tool.exe"
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run build
- run: cargo build --manifest-path src-tauri/Cargo.toml
- name: Run E2E tests (Linux)
if: matrix.os == 'ubuntu-latest'
working-directory: e2e-tests
run: npm install && xvfb-run npm test
- name: Run E2E tests (Windows)
if: matrix.os == 'windows-latest'
working-directory: e2e-tests
run: npm install && npm testundefinedname: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential \
curl wget file libxdo-dev libssl-dev \
libayatana-appindicator3-dev librsvg2-dev \
webkit2gtk-driver xvfb
- uses: dtolnay/rust-action@stable
- run: cargo install tauri-driver --locked
- name: Setup Windows WebDriver
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
cargo install --git https://github.com/chippers/msedgedriver-tool
& "$HOME/.cargo/bin/msedgedriver-tool.exe"
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run build
- run: cargo build --manifest-path src-tauri/Cargo.toml
- name: Run E2E tests (Linux)
if: matrix.os == 'ubuntu-latest'
working-directory: e2e-tests
run: npm install && xvfb-run npm test
- name: Run E2E tests (Windows)
if: matrix.os == 'windows-latest'
working-directory: e2e-tests
run: npm install && npm testundefinedBest Practices
最佳实践
Mock Testing
Mock测试
- Always call in
clearMocks()to prevent state leakageafterEach - Use spies to verify IPC calls were made correctly
- Mock at the right level: IPC for commands, windows for multi-window logic
- 始终在中调用
afterEach,防止状态泄漏clearMocks() - 使用Spy验证IPC调用是否正确执行
- 在合适的层级Mock:命令用IPC,多窗口逻辑用窗口Mock
WebDriver Testing
WebDriver测试
- Use debug builds for faster iteration during development
- Set appropriate timeouts as Tauri apps may need time to initialize
- Wait for elements explicitly rather than using implicit waits
- Keep tests independent so each test works in isolation
- 开发期间使用Debug构建以加快迭代速度
- 设置合适的超时时间,因为Tauri应用可能需要时间初始化
- 显式等待元素,而非使用隐式等待
- 保持测试独立性,确保每个测试可单独运行
CI Integration
CI集成
- Use on Linux for headless WebDriver testing
xvfb-run - Match Edge Driver version on Windows to avoid connection issues
- Build the app before running WebDriver tests
- Run unit tests before e2e tests to catch issues early
- Linux上使用实现无头WebDriver测试
xvfb-run - Windows上匹配Edge Driver版本,避免连接问题
- 运行WebDriver测试前先构建应用
- 先运行单元测试再运行端到端测试,尽早发现问题
Troubleshooting
故障排除
WebDriver Connection Timeout
WebDriver连接超时
- Windows: Verify Edge Driver version matches installed Edge
- Linux: Ensure is installed
webkit2gtk-driver - Check is running and listening on port 4444
tauri-driver
- Windows:验证Edge Driver版本与已安装的Edge匹配
- Linux:确保已安装
webkit2gtk-driver - 检查是否在运行并监听4444端口
tauri-driver
Mock Not Working
Mock不生效
- Import before the code under test
@tauri-apps/api/mocks - Call in
clearMocks()to reset stateafterEach - Ensure is properly mocked in setup
window.__TAURI_INTERNALS__
- 在被测代码前导入
@tauri-apps/api/mocks - 在中调用
afterEach重置状态clearMocks() - 确保在配置文件中正确Mock
window.__TAURI_INTERNALS__
CI Failures
CI执行失败
- Linux: Add prefix to test commands
xvfb-run - Windows: Install Edge Driver via
msedgedriver-tool - Increase timeout for slower CI runners
- Linux:在测试命令前添加前缀
xvfb-run - Windows:通过安装Edge Driver
msedgedriver-tool - 为较慢的CI runner增加超时时间