testing-tauri-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing 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:
  1. Unit/Integration Testing - Uses a mock runtime without executing native webview libraries
  2. End-to-End Testing - Uses WebDriver protocol for browser automation
Tauri支持两种主要的测试方法:
  1. 单元/集成测试 - 使用模拟运行时,无需执行原生WebView库
  2. 端到端测试 - 使用WebDriver协议实现浏览器自动化

Mocking Tauri APIs

Mock Tauri API

The
@tauri-apps/api/mocks
module simulates a Tauri environment during frontend testing.
@tauri-apps/api/mocks
模块可在前端测试期间模拟Tauri环境。

Install Mock Dependencies

安装Mock依赖

bash
npm install -D vitest @tauri-apps/api
bash
npm install -D vitest @tauri-apps/api

Mock 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
tauri-driver
to automate Tauri applications.
WebDriver测试使用
tauri-driver
实现Tauri应用的自动化。

Platform Support

平台支持

PlatformSupportNotes
WindowsFullRequires Microsoft Edge Driver
LinuxFullRequires WebKitWebDriver
macOSNoneWKWebView lacks WebDriver tooling
平台支持情况说明
Windows完全支持需要Microsoft Edge Driver
Linux完全支持需要WebKitWebDriver
macOS不支持WKWebView缺少WebDriver工具链

Install tauri-driver

安装tauri-driver

bash
cargo install tauri-driver --locked
bash
cargo install tauri-driver --locked

Platform Dependencies

平台依赖

bash
undefined
bash
undefined

Linux (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"
undefined
cargo install --git https://github.com/chippers/msedgedriver-tool & "$HOME/.cargo/bin/msedgedriver-tool.exe"
undefined

WebdriverIO Setup

WebdriverIO配置

Project Structure

项目结构

my-tauri-app/
├── src-tauri/
├── src/
└── e2e-tests/
    ├── package.json
    ├── wdio.conf.js
    └── specs/
        └── app.spec.js
my-tauri-app/
├── src-tauri/
├── src/
└── e2e-tests/
    ├── package.json
    ├── wdio.conf.js
    └── specs/
        └── app.spec.js

Package 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
undefined
yaml
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 test
undefined
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 test
undefined

Best Practices

最佳实践

Mock Testing

Mock测试

  • Always call
    clearMocks()
    in
    afterEach
    to prevent state leakage
  • 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
    xvfb-run
    on Linux for headless WebDriver testing
  • 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上使用
    xvfb-run
    实现无头WebDriver测试
  • Windows上匹配Edge Driver版本,避免连接问题
  • 运行WebDriver测试前先构建应用
  • 先运行单元测试再运行端到端测试,尽早发现问题

Troubleshooting

故障排除

WebDriver Connection Timeout

WebDriver连接超时

  • Windows: Verify Edge Driver version matches installed Edge
  • Linux: Ensure
    webkit2gtk-driver
    is installed
  • Check
    tauri-driver
    is running and listening on port 4444
  • Windows:验证Edge Driver版本与已安装的Edge匹配
  • Linux:确保已安装
    webkit2gtk-driver
  • 检查
    tauri-driver
    是否在运行并监听4444端口

Mock Not Working

Mock不生效

  • Import
    @tauri-apps/api/mocks
    before the code under test
  • Call
    clearMocks()
    in
    afterEach
    to reset state
  • Ensure
    window.__TAURI_INTERNALS__
    is properly mocked in setup
  • 在被测代码前导入
    @tauri-apps/api/mocks
  • afterEach
    中调用
    clearMocks()
    重置状态
  • 确保在配置文件中正确Mock
    window.__TAURI_INTERNALS__

CI Failures

CI执行失败

  • Linux: Add
    xvfb-run
    prefix to test commands
  • Windows: Install Edge Driver via
    msedgedriver-tool
  • Increase timeout for slower CI runners
  • Linux:在测试命令前添加
    xvfb-run
    前缀
  • Windows:通过
    msedgedriver-tool
    安装Edge Driver
  • 为较慢的CI runner增加超时时间

References

参考资料