CodeceptionでWebPayを利用するアプリケーションをテストする

WebPayは開発者が安心して効率よく開発を進めるための手段として、自動テストを重視しています。

EC-CUBE決済モジュールmdl_webpayの自動統合テストを記述した経験をもとに、 WebPayを利用したアプリケーションをCodeceptionのAcceptance Testでテストする方法を紹介します。

今回は対象のアプリケーションとして、以前の記事、「少しのPHPのコードでWebPayを導入する」で作成したものを利用します。

本記事の内容は、現在の実装に依存したハックを多数含みます。 この記事で説明している内容が将来的にも利用できることをお約束するものではないことをご理解ください。 公式ページのドキュメントで解説している項目以外は、予告なく変更することがあります。 あくまでテストのひとつの方法として、参考にしてください。

SUTを分析する

今回はテスト対象のアプリケーションがすでに決まっているので、その仕様を理解し、期待どおりの振る舞いをするかどうかを確認することにします。 「少しのPHPのコードでWebPayを導入する」を未読の場合は、先に目をとおしておいてください。

このアプリケーションでは、

  • CheckoutHelperでカード情報の入力をうけつける
  • 作成されたトークンで課金作成APIを呼ぶ
  • エラーが起きたらエラー内容を表示する

ということをしています。

CheckoutHelperはWebPayが提供しているものなので、自動テストで動作を確認する必要はありません(もちろんWebPay内部ではCIサーバで常時テストをしています)。 そこで、次の項目をテストしたいです。

  • 課金に成功したら、「お支払いありがとうございました」と表示されること
  • 課金に失敗したら、エラー情報が表示されること

実際には失敗の全パターンをテストし、カバレッジを高めるべきですが、 本記事はチュートリアルなので、もっともよく発生するカードが拒否されて支払いができない場合のみテストします。 カードエラーと500系のサーバエラー、そして通信エラーは、どれほど完璧にコーディングしても、つねに発生しうるということを念頭に置いてください。

アプリケーションコードを記述する

ベースの記事では、簡単に導入するため、WebPay側でパッケージングしたライブラリを利用していますが、今回はComposerを利用します。 PHP環境はCodeception 2を動作させるために5.4以上が必要です。 PHPやComposerのセットアップについてはとくに説明しないので、各自調べてください。

まずはcomposer initし、composer.jsonを記述していきます。

1
2
3
4
5
6
7
8
9
10
{
    "require": {
        "webpay/webpay": "~2.1"
    },
    "require-dev": {
        "codeception/codeception": "*",
        "codeception/phpbuiltinserver": "*",
        "predis/predis": "*"
    }
}

composer init · 7c614ee

Codeceptionとpredisはテストで利用します。 上記を記述後composer installしてください。

次にベース記事からアプリケーション部分の記述を拝借してきます。 SUTとテストが区別しやすいよう、appというディレクトリの下にファイルを配置します。

Create application from minimum-introduction-php · cc0797c

appディレクトリ内でbuilt-in serverを起動し、ただしく動作していることを確認しましょう。

1
php -S localhost:8080

Codeceptionをセットアップする

Codeceptionをbootstrapし、ChargeSucceededChargeFailedというCeptを追加します。 CeptとはCodeceptionのテストの単位です。

UnitやFunctionalなどのファイルも作成されますが、今回つかわないので削除しています。

Bootstrap codeception · 03c5487

bootstrapができたら、codeceptionの設定をします。 codeception.ymltests/acceptance.suite.ymlを編集し、

  • PHPBrowserではなくWebDriverを利用する
  • PhpBuiltinServer extension でダブルサーバを起動する

ように変更します。 CheckoutHelperがJavaScriptで動作するため、WebDriverを利用するのは必須です。 WebDriverのブラウザはghostdriver(phantomjs)を指定していますが、好みのものを利用してください。

1
2
3
4
5
6
7
8
extensions:
    enabled:
        - Codeception\Extension\PhpBuiltinServer
    config:
        Codeception\Extension\PhpBuiltinServer:
            hostname: localhost
            port: 8000
            router: _data/webpay_double.php
1
2
3
4
5
6
7
8
9
class_name: AcceptanceTester
modules:
    enabled:
        - WebDriver
        - AcceptanceHelper
    config:
      WebDriver:
        url: 'http://localhost:8080'
        browser: phantomjs

Configure codeception · b3cf780

さらに、テスト中にWebPayのサーバにリクエストを送信するのは好ましくないので、WebPayのサーバのように振る舞うダブルサーバを用意します。 tests/_data/webpay_double.phpに次のコードを配置してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
// Mock server to pool requests and responses
// Test cases add stub responses then rake actual requests
// Use redis to communicate with the test

require_once(__DIR__ . '/../../vendor/autoload.php');

$errorResponseJson = '{"error":{"message":"Mock response is not prepared","type":"api_error","caused_by":"service"}}';

$client = new Predis\Client();
$request = array(
    'method' => $_SERVER['REQUEST_METHOD'],
    'request_uri' => $_SERVER['REQUEST_URI'],
    'body' => file_get_contents("php://input"),
);
$client->rpush('mdl_webpay_test_requests', serialize($request));
$head = $client->lpop('mdl_webpay_test_responses');
if ($head === NULL) {
    http_response_code(500);
    header('Content-Type: application/json');
    print($errorResponseJson);
} else {
    $response = unserialize($head);
    http_response_code($response['status_code']);
    header('Content-Type: application/json');
    print($response['body']);
}

Create webpay double server script · 7cc0aab

非常に簡単なスクリプトですが、SUTから送信されたリクエストにダミーのレスポンスを返します。 Redis経由でテストとやり取りし、返すべきレスポンスや、送信されたリクエストを共有します。

テストプロセスとSUTのサーバプロセスが同一である場合はWebPayのライブラリのメソッドを置換したり、 Guzzleのモック機能を利用することができますが、 今回はacceptance testであり、これらがバラバラです。 そこでもう一つダミーサーバのプロセスを用意し、SUTとダミーサーバはHTTPで、テストプロセスとダミーサーバはRedisで通信する構成をとりました。 Redisではなく他のデータベースを使うこともできます。

じつは、アプリケーションのコードに一点、次のような変更を加えています。 WEBPAY_API_BASE環境変数をSUTのサーバを起動するときに指定することで、接続先をダミーサーバに切り替えるためです。

1
2
3
4
// WebPayインスタンスを非公開鍵で作成
// 宛先サーバを configurable にする
$apiBase = getenv('WEBPAY_API_BASE');
$webpay = $apiBase ? new WebPay(SECRET_KEY, ['api_base' => $apiBase]) : new WebPay(SECRET_KEY);

正常系のテストを記述する

テストのルールとして、正常ケースから記述します。 Codeceptionはとても可読性が高いDSLを提供しているので、読めば大体なにをしているかわかるでしょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
$I = new AcceptanceTester($scenario);
$I->wantTo('pay 800 yen with my card');

$I->amOnPage('/');
$I->fillField('amount', '800');

// ボタンがJSでロードされるのを待つ
$I->waitForElement('#WP_checkoutBox', 5);
// テストモードを有効にし、カードデータを送信しない
$I->executeJS('WebPay.testMode = true');

// ダイアログを開いてカード情報を入力
$I->click('カードで支払う');
$I->fillField('#WP_cardNumber', '4242 4242 4242 4242');
$I->selectOption('#WP_expMonth', '12');
$I->selectOption('#WP_expYear', '19');
$I->fillField('#WP_name', 'TEST TEST');
$I->fillField('#WP_cvc', '123');
$I->click('#WP_sendButton');

$I->see('お支払いありがとうございました');
$I->see('お支払い金額: 800');
$I->see('カード名義: TEST TEST');
$I->see('カード番号: ****-****-****-4242');

Implement charge succeeded test case · 2782d9a

実行してみましょう。サーバとweb driverは別のターミナルウィンドウで立ち上げることをお勧めします。

1
2
3
4
5
6
# SUTサーバ
$ WEBPAY_API_BASE=http://localhost:8000 php -S localhost:8080
# web driver
$ phantomjs  --webdriver=4444
# テスト実行
$ vendor/bin/codecept run acceptance

しかし、次のようなメッセージが出て失敗してしまいます。

1
2
3
4
5
1) Failed to pay 800 yen with my card in ChargeSucceededCept (tests/acceptance/ChargeSucceededCept.php)
Couldn't see "お支払いありがとうございました":
Failed asserting that   /charge.php
-->ApiException Message is:ApiException: Mock response is not prepared Error
--> contains "お支払いありがとうございました".

これ以外のメッセージ(ApiConnectionExceptionなど)が出る場合はセットアップに失敗しています。 リポジトリの内容を参考に修正してください。

失敗する理由は、ダブルサーバにダミーレスポンスデータを登録していないからです。 登録する方法は、当然ながらCodeceptionには用意されていません。 AcceptanceHelperにメソッドを追加することにしました。 acceptance testの$Iからメソッドが見えるようになります。

Control double server via helper · d8f323e

追加したメソッドを、テストから呼び出すようにします。

1
2
3
4
5
6
7
8
9
$I->pushMockResponse(201, [
    'id' => 'ch_abcdefghijklmno',
    'object' => 'charge',
    'livemode' => false,
    'currency' => 'jpy',
    'description' => '',
    'amount' => 800,
    ...
]);

Add dummy response · f682b6e

再度実行すると、テストが通ることを確認できます。

しかし、画面に表示する内容を確認しただけで、リクエスト内容はまだ確認していません。 レスポンスに固定の800円という金額を指定していますが、じつは10000円でリクエストされていたら大変です。

ダブルサーバが受信したリクエストの中身も確認しましょう。 先程AcceptanceHelperに追加しておいたメソッドを利用します。 一番最後に次のような確認を追加します。

1
2
3
4
5
6
7
8
// リクエスト内容をチェック
// seeInDataはデータ型も見る
$I->loadRequest();
$I->seeRequestTo('POST', '/charges');
$I->seeInData('amount', 800);
$I->seeInData('currency', 'jpy');
$I->seeInData('card');  // cardパラメータが指定されていること
$I->seeInData('description', 'PHP からのアイテムの購入');

Assert request · 63a8631

変更しても、テストが通るはずです。 ためしにamountを変えてみると、テストが失敗し、ただしくテストできていることが分かります。

カードエラー時の表示を確認する

カードエラーは実際の運用では頻繁に発生するものの、製作途中で目にすることが少ないため、見落としがちな項目のひとつです。 レスポンスを自由に差し替えられるダブルサーバなら簡単に再現にできますので、かならずテストしておきましょう。

WebPayのテスト環境でカードエラーを発生させるにはテスト環境で使用できるクレジットカードを利用してください。 自動テストをしていても、かならずテストカード番号をもちいた確認もしてください。

今回は残高不足などでカード払いが受理されなかったときのエラーをテストします。

テストのストーリーは成功ケースとほとんど同じです。 レスポンスと、確認するテキストだけエラーケースのものに変更します。 リクエスト内容はすでにテストしているので、こちらではしません。

テストに重複が多くて嫌だと思った方は、CodeceptionのStepObjectを利用しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// エラーレスポンスを返す
$I->pushMockResponse(402, [
    'error' => [
        'message' => 'このカードでは取引をする事が出来ません。利用出来ない理由をご契約中のカード会社へお問い合わせるか、他のカードをご利用ください。',
        'caused_by' => 'buyer',
        'type' => 'card_error',
        'code' => 'card_declined'
    ]
]);

$I->click('#WP_sendButton');

$I->see('Status is:402');
$I->see('Type is:card_error');
$I->see('Code is:card_declined');
$I->see('Message is:このカードでは取引をする事が出来ません。利用出来ない理由をご契約中のカード会社へお問い合わせるか、他のカードをご利用ください。');

Test card error · a555055

これを実行すると、

1
2
3
4
5
6
1) Failed to see descriptive error if my card is declined in ChargeFailedCept (/home/tomita/webpay/codeception-and-webpay/tests/acceptance/ChargeFailedCept.php)
Couldn't see "Message is:このカードでは取引をする事が出来ません。利用出来ない理由をご契約中のカード会社へお問い合わせるか、他のカードをご利用ください。":
Failed asserting that   /charge.php
-->Status is:402 Type is:card_error Code is:card_declined Param is: Message is:このカードでは取引をする事が出来ません。利用出来ない理由をご契約ä¸[39m
[Content too long to display. See complete response in '_output' directory]
--> contains "message is:このカードでは取引をする事が出来ません。利用出来ない理由をご契約中のカード会社へお問い合わせるか、他のカードをご利用ください。".

おっと、文字化けしていますね。 環境依存なので、もしかしたら通ってしまうかもしれません。 文字化けしていては、$I->wantTo('see descriptive error if my card is declined')という受け入れ項目が満たされません。

1
header('Content-Type: text/plain; charset=utf-8');

app/charge.phpに追加することで直りました。

Specify content type · 567475e

たぶん、まだ他のエラーケースは文字化けしています。 各自テストを書いて、修正してみてください。

おわりに

冒頭で紹介したEC-CUBEモジュールのテストを書いていて、PHPの特性に悩まされまくったので、自分なりのやりかたをまとめました。 私はあまりPHPに堪能ではないので、もっと優れた方法があるかもしれません。 テスト環境としての使い勝手はそれなりによい物になっていると思います。

CodeceptionのAcceptanceTestはサーバプロセスとテストプロセスが完全に分離して存在する都合上、 プロセス間のやり取りをするにはトリックが必要になります。 そのためにRedisを利用したり、ダブルサーバを用意したり、グルーコードをたくさん書くことになりました。 レスポンスもWebPayのテスト環境で試したものを元に加工するなど、手間がかかります。

ライブラリをモックした単体テストでは、よりテスト対象に的をしぼったテストができます。 CodeceptionにはUnitTest、FunctionalTestの機能も備わっています。 分岐が多く、複雑な箇所は粒度のこまかいテスト方法を利用し、全体の流れをテストするためにAcceptanceTestを、というふうに使い分けることで、 簡潔なテストコードで大きな成果を得られます。

いろいろなテスト方法を使いわけ、アプリケーションを効率的に開発し、安定して運用することを目指しましょう。