テストツール

TestCafe

概要

TestCafeはDevExpress社開発のゼロ設定が可能なE2Eテストフレームワークで、ブラウザドライバーのインストールや設定が不要な革新的なWebアプリケーション自動化ツールです。JavaScript/TypeScriptで記述でき、Chrome、Firefox、Safari、Edge、Internet Explorerを含む全てのモダンブラウザに対応。ライブモード、並列実行、自動要素待機、スマートアサーション、APIテスト機能により、開発者にとって使いやすく高性能なEnd-to-Endテストとインテグレーションテストを実現します。Node.jsベースで動作し、独自のプロキシサーバー技術により安定したクロスブラウザテストと高速なテスト実行を提供する次世代のWebテスト自動化ソリューションです。

詳細

TestCafe 2025年版は、ゼロ設定思想を貫くことで複雑なセットアップを不要にし、開発者の生産性を最大化するE2Eテストフレームワークです。WebDriverやブラウザドライバーのインストール・設定が一切不要で、npmインストール後すぐにテスト実行が可能。独自開発のプロキシサーバー技術により、ブラウザとテストコード間の通信を制御し、従来のSeleniumベースツールで発生しがちなフラキーテストを根本的に解決します。ライブモード機能では、テストファイルの変更を自動検知してリアルタイムでテスト再実行し、開発サイクルを大幅に高速化。並列実行とコンカレンシー制御により、大規模テストスイートも効率的に実行可能。スマートアサーション機能では要素の状態変化を自動待機し、明示的な待機コードを削減。APIテスト機能の統合により、UIテストとAPIテストを単一フレームワークで実行でき、包括的なアプリケーションテストを実現します。

主な特徴

  • ゼロ設定アーキテクチャ: ブラウザドライバーやWebDriverの設定が不要で即座にテスト開始
  • 独自プロキシ技術: プロキシサーバーによる安定したブラウザ制御とフラキーテスト削減
  • ライブモード: テストファイル変更の自動検知とリアルタイム再実行
  • 並列実行と高速化: 複数ブラウザ・インスタンスでの同時テスト実行
  • スマートアサーション: 要素状態の自動待機と動的アサーション
  • APIテスト統合: UIテストとAPIテストの統合実行環境

メリット・デメリット

メリット

  • ゼロ設定でのシンプルなセットアップとブラウザドライバー管理の不要
  • 独自プロキシ技術による高い安定性とフラキーテストの大幅削減
  • ライブモード機能による高速な開発・デバッグサイクルの実現
  • JavaScript/TypeScript環境での一貫した開発体験と既存スキル活用
  • 並列実行とコンカレンシー制御による大規模テストの効率化
  • スマートアサーションと自動待機による簡潔なテストコード記述
  • Chrome Device Emulation、Headlessモード、カスタムブラウザ引数対応
  • APIテスト機能による包括的なアプリケーション品質保証

デメリット

  • JavaScriptエコシステムに限定され、他言語(Java、Python、C#等)は非対応
  • PlaywrightやSeleniumと比較してコミュニティとエコシステムが小さい
  • 複雑なSPAアプリケーションでの一部動作制限とカスタム設定の必要性
  • モバイルアプリのネイティブテストや特殊なブラウザ環境は非対応
  • DevExpress社の商用製品のため、企業ライセンス管理の考慮が必要
  • Seleniumと比較してサードパーティ拡張機能や統合ツールが限定的

参考ページ

書き方の例

インストールとセットアップ

# TestCafeのグローバルインストール
npm install -g testcafe

# プロジェクトローカルへのインストール
npm install --save-dev testcafe

# テスト実行(Chromeブラウザ)
testcafe chrome test1.js

# ヘッドレスモードでのテスト実行
testcafe chrome:headless tests/sample-fixture.js

# 複数ブラウザでの並列テスト実行
testcafe chrome,firefox tests/

# ライブモードでの開発(ファイル変更の自動検知)
testcafe chrome tests/test.js -L

# 並列実行数の指定
testcafe -c 3 chrome tests/test.js

# デバッグモードでの実行
testcafe chrome my-tests/**/*.js --debug-mode

基本的なE2Eテスト

// test/basic-e2e.js
import { Selector } from 'testcafe';

fixture `基本的なE2Eテスト`
    .page `https://devexpress.github.io/testcafe/example`;

test('フォーム入力とアサーション', async t => {
    const nameInput = Selector('#developer-name');
    const submitButton = Selector('#submit-button');
    const articleHeader = Selector('#article-header');

    await t
        .typeText(nameInput, '田中太郎')
        .click(submitButton)
        .expect(articleHeader.innerText).eql('Thank you, 田中太郎!');
});

test('要素の状態確認', async t => {
    const nameInput = Selector('#developer-name');
    const triedTestCafeCheckbox = Selector('#tried-test-cafe');
    const featureSelect = Selector('#preferred-interface');

    await t
        // テキスト入力
        .typeText(nameInput, 'TestCafe User')
        .expect(nameInput.value).eql('TestCafe User')
        
        // チェックボックス操作
        .click(triedTestCafeCheckbox)
        .expect(triedTestCafeCheckbox.checked).ok()
        
        // セレクトボックス操作
        .click(featureSelect)
        .click(featureSelect.find('option').withText('Both'))
        .expect(featureSelect.value).eql('Both');
});

test('条件付きテストとエラーハンドリング', async t => {
    const errorMessage = Selector('.error-message');
    const submitButton = Selector('#submit-button');

    // 空のフォーム送信
    await t.click(submitButton);

    // エラーメッセージの確認
    if (await errorMessage.exists) {
        await t.expect(errorMessage.visible).ok();
        await t.expect(errorMessage.innerText).contains('必須項目');
    }
});

フォーム操作とバリデーション

// test/form-validation.js
import { Selector } from 'testcafe';

fixture `フォームバリデーションテスト`
    .page `https://example.com/contact`;

test('複雑なフォーム操作', async t => {
    const form = Selector('#contact-form');
    const nameField = Selector('#name');
    const emailField = Selector('#email');
    const messageField = Selector('#message');
    const categorySelect = Selector('#category');
    const agreeCheckbox = Selector('#agree-terms');
    const submitButton = Selector('#submit');
    const successMessage = Selector('.success-message');

    await t
        // フォーム要素の存在確認
        .expect(form.exists).ok()
        
        // 入力フィールドの操作
        .typeText(nameField, '山田太郎')
        .typeText(emailField, '[email protected]')
        .typeText(messageField, 'TestCafeを使ったお問い合わせテストです。')
        
        // セレクトボックスの選択
        .click(categorySelect)
        .click(categorySelect.find('option').withText('技術サポート'))
        
        // チェックボックス操作
        .click(agreeCheckbox)
        .expect(agreeCheckbox.checked).ok()
        
        // フォーム送信
        .click(submitButton)
        
        // 成功メッセージの確認
        .expect(successMessage.exists).ok()
        .expect(successMessage.innerText).contains('送信完了');
});

test('バリデーションエラーのテスト', async t => {
    const emailField = Selector('#email');
    const submitButton = Selector('#submit');
    const emailError = Selector('#email-error');

    await t
        // 無効なメールアドレスの入力
        .typeText(emailField, 'invalid-email')
        .click(submitButton)
        
        // バリデーションエラーの確認
        .expect(emailError.exists).ok()
        .expect(emailError.innerText).contains('正しいメールアドレス');

    // 正しいメールアドレスに修正
    await t
        .selectText(emailField)
        .typeText(emailField, '[email protected]')
        .expect(emailError.exists).notOk();
});

test('ドラッグアンドドロップ操作', async t => {
    const fileIcon = Selector('.file-icon');
    const dropZone = Selector('.drop-zone');

    await t
        .dragToElement(fileIcon, dropZone, {
            offsetX: 10,
            offsetY: 10,
            destinationOffsetX: 100,
            destinationOffsetY: 50
        })
        .expect(dropZone.hasClass('file-dropped')).ok();
});

APIテストとUIテストの統合

// test/api-ui-integration.js
import { Selector, RequestHook } from 'testcafe';

// APIリクエストのモック
class MockAPIHook extends RequestHook {
    constructor() {
        super('https://api.example.com', {
            includeHeaders: true,
            includeBody: true
        });
    }

    async onRequest(event) {
        console.log(`API Request: ${event.request.method} ${event.request.url}`);
    }

    async onResponse(event) {
        console.log(`API Response: ${event.response.statusCode}`);
    }
}

const mockAPI = new MockAPIHook();

fixture `APIとUIの統合テスト`
    .page `https://example.com/dashboard`
    .requestHooks(mockAPI);

test('UIからのAPIデータ取得テスト', async t => {
    const loadButton = Selector('#load-data');
    const dataContainer = Selector('.data-container');
    const loadingSpinner = Selector('.loading');

    await t
        // データ読み込みボタンをクリック
        .click(loadButton)
        
        // ローディング状態の確認
        .expect(loadingSpinner.exists).ok()
        
        // データの表示確認(APIレスポンス待機)
        .expect(dataContainer.exists).ok()
        .expect(dataContainer.find('.data-item').count).gte(1);
});

test('APIリクエストの直接実行', async t => {
    // TestCafeのAPIリクエスト機能(v1.20.0以降)
    const responseBody = await t.request('http://localhost:3000/api/users').body;
    
    await t
        .expect(responseBody).contains('users')
        .expect(JSON.parse(responseBody).length).gte(1);
});

test('UIとAPIの状態同期テスト', async t => {
    const createButton = Selector('#create-item');
    const itemsList = Selector('.items-list');
    const newItemTitle = 'テストアイテム';

    // UI経由でアイテム作成
    await t
        .click(createButton)
        .typeText('#item-title', newItemTitle)
        .click('#save-item');

    // UI上での表示確認
    await t.expect(itemsList.find('.item').withText(newItemTitle).exists).ok();

    // APIエンドポイントで実際のデータ確認
    const apiResponse = await t.request('http://localhost:3000/api/items').body;
    const items = JSON.parse(apiResponse);
    const createdItem = items.find(item => item.title === newItemTitle);
    
    await t.expect(createdItem).ok();
});

カスタムセレクターとスマートアサーション

// test/custom-selectors.js
import { Selector, ClientFunction } from 'testcafe';

// カスタムセレクター定義
const byDataTestId = (id) => Selector(`[data-testid="${id}"]`);
const byRole = (role) => Selector(`[role="${role}"]`);

// カスタムクライアント関数
const getWindowLocation = ClientFunction(() => window.location.toString());
const getBrowserConsoleMessages = ClientFunction(() => 
    window.console._logs || []
);

fixture `カスタムセレクターとアサーション`
    .page `https://example.com/app`;

test('カスタムセレクターの活用', async t => {
    const loginButton = byDataTestId('login-button');
    const userMenu = byRole('menu');
    const headerSection = Selector('header').find('.user-info');

    await t
        // data-testid属性を使った要素選択
        .expect(loginButton.exists).ok()
        .click(loginButton)
        
        // role属性を使った要素選択
        .expect(userMenu.visible).ok()
        
        // ネストしたセレクター
        .expect(headerSection.find('.username').innerText).eql('testuser');
});

test('スマートアサーションと動的要素', async t => {
    const dynamicContent = Selector('#dynamic-content');
    const loadingIndicator = Selector('.loading');

    await t
        // ローディングインジケーターの出現を待機
        .expect(loadingIndicator.exists).ok()
        
        // 動的コンテンツの表示を自動待機
        .expect(dynamicContent.innerText).contains('データ読み込み完了')
        
        // ローディングインジケーターの消失を確認
        .expect(loadingIndicator.exists).notOk();
});

test('ClientFunctionを使ったブラウザ状態確認', async t => {
    const currentLocation = await getWindowLocation();
    
    await t
        .expect(currentLocation).eql('https://example.com/app')
        
        // ページナビゲーション
        .click('#about-link');

    const newLocation = await getWindowLocation();
    await t.expect(newLocation).contains('/about');
});

test('条件付きテスト実行', async t => {
    const advancedFeature = Selector('#advanced-feature');
    
    // 要素の存在確認後の条件分岐
    if (await advancedFeature.exists) {
        await t
            .click(advancedFeature)
            .expect(Selector('.advanced-panel').visible).ok();
    } else {
        console.log('Advanced feature not available in this environment');
    }
});

ライブモードとデバッグ機能

// test/debug-features.js
import { Selector } from 'testcafe';

fixture `デバッグ機能の活用`
    .page `https://example.com/debug`;

test('デバッグ機能の使用例', async t => {
    const debugButton = Selector('#debug-button');
    
    // デバッグポイントの設定(実行一時停止)
    await t.debug();
    
    await t
        .click(debugButton)
        .expect(Selector('#result').innerText).eql('Debug Success');
});

test('コンソールメッセージの確認', async t => {
    // ブラウザコンソールメッセージの取得
    const consoleMessages = await t.getBrowserConsoleMessages();
    
    await t
        .click('#generate-error')
        .expect(consoleMessages.error).contains('Test error message');
});

test('スクリーンショットの取得', async t => {
    await t
        .click('#action-button')
        .takeScreenshot('action-result.png')
        .expect(Selector('#success-message').visible).ok();
});

test('ブラウザ情報の取得', async t => {
    // ブラウザ情報の確認
    const isNativeAutomation = t.browser.nativeAutomation;
    const engineName = t.browser.engine.name;
    
    console.log(`Browser Engine: ${engineName}`);
    console.log(`Native Automation: ${isNativeAutomation}`);
    
    await t.expect(isNativeAutomation).ok();
});

設定ファイルと高度な機能

// .testcaferc.json
{
    "browsers": ["chrome", "firefox"],
    "src": ["tests/**/*.js"],
    "reporter": [
        {
            "name": "spec"
        },
        {
            "name": "json",
            "output": "reports/report.json"
        }
    ],
    "screenshots": {
        "path": "screenshots/",
        "takeOnFails": true,
        "pathPattern": "${DATE}_${TIME}/test-${TEST_INDEX}/${USERAGENT}/${FILE_INDEX}.png"
    },
    "concurrency": 3,
    "selectorTimeout": 10000,
    "assertionTimeout": 10000,
    "pageLoadTimeout": 8000,
    "speed": 0.1,
    "debugMode": false,
    "skipJsErrors": false,
    "quarantineMode": false,
    "disablePageReloads": false,
    "disablePageCaching": true,
    "clientScripts": [
        "helpers/mock-date.js",
        "helpers/react-helpers.js"
    ]
}
// testcafe-runner.js - プログラマティック実行
const createTestCafe = require('testcafe');
let testcafe = null;

createTestCafe('localhost', 1337, 1338)
    .then(tc => {
        testcafe = tc;
        const runner = testcafe.createRunner();

        return runner
            .src(['tests/fixture1.js', 'tests/fixture2.js'])
            .browsers(['chrome:headless', 'firefox:headless'])
            .concurrency(2)
            .reporter(['spec', {
                name: 'json',
                output: 'reports/report.json'
            }])
            .screenshots({
                path: 'screenshots/',
                takeOnFails: true
            })
            .run({
                skipJsErrors: true,
                debugMode: false,
                selectorTimeout: 10000,
                assertionTimeout: 10000
            });
    })
    .then(failedCount => {
        console.log('Tests failed: ' + failedCount);
        testcafe.close();
    })
    .catch(error => {
        console.error('Error during test execution:', error);
        if (testcafe) {
            testcafe.close();
        }
    });

CI/CD統合とDocker活用

# .github/workflows/testcafe.yml
name: TestCafe Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        browser: [chrome, firefox]
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Start application
        run: |
          npm start &
          npx wait-on http://localhost:3000
      
      - name: Run TestCafe tests
        run: testcafe ${{ matrix.browser }}:headless tests/ --reporter spec,json:reports/report-${{ matrix.browser }}.json
      
      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ matrix.browser }}
          path: |
            reports/
            screenshots/
# Dockerfile.testcafe
FROM testcafe/testcafe:latest

# カスタムヘルパースクリプトのコピー
COPY helpers/ /tests/helpers/
COPY tests/ /tests/

# TestCafe実行
CMD ["firefox", "/tests/**/*.js", "--reporter", "spec,json:/reports/report.json"]
# Docker Composeでの実行例
# docker-compose.yml
version: '3.8'
services:
  testcafe:
    build: .
    volumes:
      - ./tests:/tests
      - ./reports:/reports
    depends_on:
      - app
    environment:
      - BASE_URL=http://app:3000

  app:
    build: ./app
    ports:
      - "3000:3000"

# 実行コマンド
docker-compose up --abort-on-container-exit