はじめに
WindowsアプリケーションのUIテストの自動化用の商用ツールはありますが、現場での知見の不足とお値段的な問題でなかなか導入まで至っていないことがあり、どうしても多くの場合手作業でテストを実施してるのが現状ではないでしょうか?
これに対して、Webシステムの場合には、PlaywrightやCypress等のTypeScriptベースで自動テスト作成することが多くなっており、QAエンジニアにとってTypeScriptは、もはや定番の言語となってきた感があります。
また、Androidなどのモバイル端末の自動テストにおいては、Appiumを使っている現場が多いと思います。
本記事では、WindowアプリケーションのUIテストの自動化に、TypeScript + Appium + Windows Application Driverを使うことで、QAエンジニアが自動するための勘所を示します。
Appium + Windows Application Driverとは?
Appiumは、モバイルアプリのテストにてよく使われているUIテストを自動化するための、オープンソースのフレームワークです。
Appiumでは、Driverという仕組みを使うことで、同じテストコードに対して、iOS用のDriverを使えばiOSのアプリのテストが、Android用のDriverを使えば、Androidのアプリのテストができるといったクロスプラットフォーム対応のフレームワークです。同時に、Python、Java、JavaScriptなどの主要な言語に対応しています。
Windows Application Driverは、このAppiumにてWindowsアプリのUIテストを自動化するためのDriverであり、マイクロソフトがGitHubにて公開しています。(ただし、2021年からメンテナンスされていません)
環境構築
Windows Application Driverのインストール
- 開発者モードの有効化
- 設定 → 更新とセキュリティ → 開発者向け → 「開発者モード」をオン にする。(これをしないとWinAppDriverが動作しません)
- Windows Application Driver(WinAppDriver)のインストール
- WinAppDriver GitHub Releases から .msi ファイルをダウンロードしてインストールしてください
Appiumの環境構築
ターミナル(PowerShellやコマンドプロンプト)を開き、以下の順に実行してください。
# Appiumサーバーのインストール
npm install -g appium
# Windows用のドライバーのインストール
appium driver install windows
プロジェクトディレクトリの作成と初期化
テストコードを管理するフォルダを作成して、WebdriverIOの設定ウィザードを起動します。
# フォルダ作成と移動
mkdir win-app-test
cd win-app-test
# WebdriverIOのセットアップウィザード起動
npm init wdio .
【重要】npm init wdio 実行中の選択肢(推奨設定):
- Where is your automation backend located? On my local machine
- Which framework do you want to use? Mocha (https://mochajs.org/) ← 一般的なので
- Do you want to use Typescript to write tests? Yes ← TypeScriptを選択(デフォルト)
- Do you want to use page objects (https://martinfowler.com/bliki/PageObject.html)? Yes ← ページオブジェクトを使うため
- Which reporter do you want to use? spec ← コンソールに見やすく出力されるため
- Do you want to add a service to your test setup? appium ← Appiumです
- Do you want me to run
npm installNo ← すでに必要なもののインストールは終わっているため
設定ファイルの書き換え ( wdio.conf.ts )
ウィザード完了後、生成された wdio.conf.ts を開いて、Windowsアプリ用に capabilities セクションを修正します。
// wdio.conf.ts の一部を以下のように修正
export const config: WebdriverIO.Config = {
// ... 他の設定はそのまま
capabilities: [{
'appium:platformName': 'Windows',
'appium:automationName': 'windows',
'appium:app': 'C:\\Windows\\System32\\notepad.exe', // テストしたいアプリのパス
// 必要に応じて以下を追加
// 'appium:deviceName': 'WindowsPC'
}],
// ...
}
動作確認
実際にテストを実行するには、3つのウインドウが必要になります。
ウインドウ1: WinAppDriverの起動(管理者権限)
インストールした WinAppDriver を直接起動します。
C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe を右クリックして「管理者として実行」してください。
(Listening on 0.0.0.0:4723 と表示されればOK)
ウインドウ2: Appiumサーバーの起動
appium
(※WinAppDriverがポート4723を使っている場合、Appium側でポートをずらすか、Appium経由ではなく直接WinAppDriverに繋ぐ設定にする必要があります。基本的にはWebdriverIOの設定で port: 4723 を指定すれば接続可能です)
ウインドウ3: テストの実行
npm run wdio
ここまでセットアップできれば、.\test\specs\**\*.ts をテストプログラムとして認識してテストが実行されるようになるため、QAエンジニアが慣れ親しんだ TypeScript にてWindowsアプリの自動テストを行うための土台は整いました。
ページオブジェクトパターンの実装例
Appiumを使ってWindowsアプリの自動テストを行うための基本である画面上の要素に対するアクセスに関しては、いろいろな方が紹介されています。
しかしながら、実際にある程度以上のボリュームのテストの実装とのメンテナンスを考慮した場合には、いちいち画面上の要素を直接アクセスするコードをテストコード本体に記述するのは、以下のような問題があります。
- テストの本質ではない画面要素にアクセスするためのコードによる問題
- 実装効率の低下
- メンテナンス性の低下
- UIの変更に脆弱なテストコードとなる問題
- メンテナンスコストの増大
このテストコード内での要素に対するアクセスの問題は、AppiumのベースとなったSeleniumのころからページオブジェクトパターンというベストプラクティスがありました。
つまり、Windowsアプリの画面そのものをページオブジェクトとして実装することで、テストコード本体と画面の要素に対するアクセスを切り離し、ページオブジェクトの再利用による実装効率の向上とUIの変更をページオブジェクトの中に隠ぺいすることで、テストコードの実装効率およびメンテナンス性の向上を図れます。
1. ページオブジェクトの実装例( pages/login.page.ts )
/**
* ログイン画面のページオブジェクト
*/
class LoginPage {
// ==========================================================================
// 要素定義 (Selectors)
// Windowsアプリでは AutomationId を使用するのが一般的です
// ==========================================================================
// ユーザーID入力フィールド
get userIdInput() {
return $('#UserIdTextField'); // AutomationId が "UserIdTextField" の場合
}
// パスワード入力フィールド
get passwordInput() {
return $('#PasswordTextField'); // AutomationId が "PasswordTextField" の場合
}
// ログインボタン
get loginButton() {
return $('#LoginButton'); // AutomationId が "LoginButton" の場合
}
// ==========================================================================
// 操作メソッド (Actions)
// ==========================================================================
/**
* ログイン処理を行う
* @param userId 入力するユーザーID
* @param password 入力するパスワード
*/
async login(userId: string, password: string): Promise<void> {
// 1. ユーザーIDを入力 (既存の値をクリアしてから入力)
await this.userIdInput.setValue(userId);
// 2. パスワードを入力
await this.passwordInput.setValue(password);
// 3. ログインボタンをクリック
await this.loginButton.click();
}
/**
* ユーザーIDが正しく入力されているか確認する (検証用)
*/
async getUserIdValue(): Promise<string> {
return await this.userIdInput.getValue();
}
}
// インスタンスをエクスポートして、テストファイルで使い回せるようにする
export default new LoginPage();
2. テストコードでの利用例 ( test/spec/login.test.ts )
login.page.tsを使ったテストコードでは「どのような操作を行うか」というビジネスロジック(テスト内容)に集中でき、非常に読みやすくなります。
また、WebdriverIOのグローバルオブジェクト機能により、ドライバーの明示的な受け渡しを排除して、ビジネスロジックに集中した実装にしています。
import LoginPage from '../pages/login.page';
describe('ログイン機能のテスト', () => {
it('正しいユーザーIDとパスワードでログインできること', async () => {
// ページオブジェクトのメソッドを呼び出すだけ
// 具体的なセレクタ (#UserIdTextField など) をここでは意識しなくて良い
await LoginPage.login('test_user_01', 'Password123!');
// ログイン後の画面遷移を確認するなどのアサーションをここに記述
// expect(await MainPage.header).toBeDisplayed();
});
it('誤ったパスワードでエラーが表示されること', async () => {
await LoginPage.login('test_user_01', 'WrongPassword');
// エラーメッセージの表示を確認するなどの処理
// const errorMsg = await ErrorPage.errorMessage;
// expect(errorMsg).toBe('ユーザーIDまたはパスワードが正しくありません。');
});
});
このようにWindowsアプリのテストにおいても、ページオブジェクトパターンを採用することで、効率的な自動テストの実装を行うことができます。
堅牢性を高めるテクニック
Windowsアプリケーションの自動テストにおいて、最も問題となるのは、テスト対象のアプリケーションがハングアップした場合と想定外の画面遷移を行って、テストコードの操作を受け付けなる場合です。
この状態となった場合には、発生したテストケースがエラーになるだけではなく、後続のテストがすべて正しく実行されないため、テスト実行そのものが不正な結果となります。
これを防ぐためには、現在実行しているテスト対象のアプリケーションを強制的に停止させる必要があります。
これは、TypeScript から PowerShell を呼び出してプロセスをKillするユーティリティ関数を用意し、before (開始前) / after (終了後)、あるいは異常系テストの途中で呼び出して環境をクリーンにすることなります。
プロセス管理ユーティリティの実装例 (utils/process-util.ts)
import { exec } from 'child_process';
import { promisify } from 'util';
const execPromise = promisify(exec);
export class ProcessUtil {
/**
* プロセスを強制終了させる
*/
static async killProcess(processName: string): Promise<void> {
console.log(`Stopping process: ${processName}...`);
const psCommand = `Stop-Process -Name "${processName}" -Force -ErrorAction SilentlyContinue`;
try {
await execPromise(`powershell -Command "${psCommand}"`);
} catch (error) {
console.error(`Kill process failed: ${error}`);
}
}
/**
* アプリケーションを指定したパスから起動する
* @param exePath 実行ファイルのフルパス (例: 'C:\\MyApp\\App.exe')
*/
static async launchApp(exePath: string): Promise<void> {
console.log(`Launching application from: ${exePath}...`);
try {
// startコマンドを使って非同期で起動(Node.js側でプロセスを待機し続けないため)
await execPromise(`start "" "${exePath}"`);
console.log('Application launch command sent.');
} catch (error) {
console.error(`Launch app failed: ${error}`);
throw error;
}
}
/**
* 完全なリセット(終了 $\rightarrow$ 起動)を行う
*/
static async resetApp(processName: string, exePath: string): Promise<void> {
await this.killProcess(processName);
// プロセスが完全に消えるまで少し待機
await new Promise(resolve => setTimeout(resolve, 2000));
await this.launchApp(exePath);
}
}
テストコードでの利用例 (test/spec/login.test.ts )
テストコードの中で、各テストが常に「クリーンに起動した状態」から開始されるように修正します。
import { ProcessUtil } from '../utils/process-util';
import LoginPage from '../pages/login.page';
describe('ログイン機能の統合テスト', () => {
// 設定値
const APP_CONFIG = {
processName: 'YourAppName', // タスクマネージャーに表示される名前(.exe抜き)
exePath: 'C:\\Program Files\\YourApp\\YourAppName.exe' // 実際のインストールパス
};
// 全テストの前に一度だけ実行する場合
before(async () => {
console.log('--- Environment Setup ---');
await ProcessUtil.resetApp(APP_CONFIG.processName, APP_CONFIG.exePath);
// アプリが完全に立ち上がり、ログイン画面が表示されるまで待機
// ページオブジェクトの要素を使って待機するのが最も確実です
await LoginPage.userIdInput.waitForDisplayed({ timeout: 10000 });
console.log('Application is ready.');
});
it('正常系:正しい資格情報でログインできること', async () => {
await LoginPage.login('admin_user', 'SecurePassword123');
// ここにログイン後の検証処理を記述
// expect(await MainPage.welcomeMessage).toBe('Welcome, Admin!');
});
it('誤ったパスワードでエラーが表示されること', async () => {
await LoginPage.login('test_user_01', 'WrongPassword');
// エラーメッセージの表示を確認するなどの処理
// const errorMsg = await ErrorPage.errorMessage;
// expect(errorMsg).toBe('ユーザーIDまたはパスワードが正しくありません。');
});
it('異常系:アプリがクラッシュしても再起動して復帰できること', async () => {
// 1. 意図的にプロセスをキル(擬似的なクラッシュ)
await ProcessUtil.killProcess(APP_CONFIG.processName);
// 2. 再起動処理
await ProcessUtil.resetApp(APP_CONFIG.processName, APP_CONFIG.exePath);
await LoginPage.userIdInput.waitForDisplayed({ timeout: 10000 });
// 3. 回復後の動作確認
await LoginPage.login('admin_user', 'SecurePassword123');
});
after(async () => {
console.log('--- Cleaning up ---');
await ProcessUtil.killProcess(APP_CONFIG.processName);
});
});
まとめ
このようなページオブジェクトパターンとプロセス管理ユーティリティを組み合わせることで、QAエンジニアが得意とするTypeScriptを使って、Windowsアプリケーションの堅牢な自動テストを実装することができるようになります。