LINE Ad SDKを利用したテスト自動化

この記事では、LINEプラットフォームで提供している広告クライアントモジュールをテストした方法をご紹介します。LINEの広告クライアントモジュールは、モバイルとWebで使用できますが、ここではモバイルクライアントのテスト方法だけを説明したいと思います。

LINE Ads Platformの概要

LINE Ads Platformは、下図のようにシンプルな構造になっています。サーバとクライアント間の通信には多様なプロトコルが使用できますが、今回のテストはHTTPプロトコルを対象にしています。

テスト環境の概要

LINE Ads Platformはサーバ-クライアント構造のシステムですが、このような環境でクライアントの多様な動作をテストする場合は、障害になる要素がある可能性があります。テストを行う主体(人間または自動化されたシステム)は、各テストを実施する度に特定の動作をサーバに期待します。

しかし、毎回サーバを修正して期待通りの動作を誘導することは効率的ではありません。特に、サーバの失敗状態をテストするための環境を構成するのは簡単なことではありません。

このような困難を解決し、テストをより容易にするために、実際のサーバの動作を模倣するツールを使用しました。HTTPをベースにした多様なモックサーバ(mock server)ツールがありますが、LINEの広告クライアントモジュールのテストにはWireMockというツールを使用しました。下図は、WireMockを用いたテスト環境をダイアグラムで表したものです。

上図にある各要素の役割は以下のとおりです。

  • Mobile Device : テスト対象のアプリケーションをインストールし、そのアプリケーションをテストするためのコードが動作する実際のスマートフォン、エミュレータまたはシミュレータ。
  • Client App : LINE広告クライアントモジュールが使われたアプリケーション。テストコードはこのアプリケーションのUIを操作して広告クライアントモジュールSDKを動作させることで、多様なシナリオのテストを実施できる。
  • SDK : LINE広告クライアントモジュール。
  • Testing App : テストコードを含んでいるバイナリ。
  • Testing Helper: テストコードで使用するモジュール。テストで期待される動作をサーバに設定し、各リクエストに対する有効性を検証するために情報をサーバに依頼する機能を提供する。
  • Server PC : モックサーバが動作しているPC。
  • Server App : クライアントのテストに必要なサーバの動作を実装したサーバアプリケーションで、Javaで作成されている。基本的にはWireMockのHTTP Mock機能を使用し、自動化されたテストを実施するために必要な一部の機能を追加することもある。
  • WireMock : HTTPベースのサーバAPIを簡単に定義し使用できるようにサポートするオープンソースツール。
  • Mappings : 特定のリクエストに対するレスポンスを定義しているファイル。WireMockで使用し、JSON形式で作成される。
  • Response files : Mappingsに定義された一部のレスポンスを外部ファイルとして提供するときに使用する。WireMockで使用し、テキストファイルで作成される。LINE広告クライアントモジュールではJSON形式で作成されたファイルのみ使用する。

WireMockの紹介

WireMockの公式Webサイトでは、WireMockは、HTTPベースのAPIのためのシミュレータであり、サービス仮想化ツールまたはモックサーバとして考慮する価値があると紹介しています。

WireMockが提供する機能は以下のとおりです。

  • Stubbing : 事前に定義したマッチング情報を基にリクエストに対するHTTPレスポンスを定義し、適切なレスポンスを返す機能を提供する。
  • Verifying : WireMockは受信したすべてのリクエスト情報を記憶し、特定のリクエストの受信有無および詳細情報を確認できる機能を提供する。
  • Request Matching : URL、HTTPメソッド、Headersなど多様な属性情報を利用してリクエストを識別する機能を提供する。
  • Proxying : 特定のリクエストを別のホストに転送できるプロキシ機能を提供する。
  • Record and Playback : 転送されたリクエストとレスポンスを記録し、ファイルで保存する機能を提供する。
  • Simulating Faults : エラーコードを含むHTTPレスポンスまたは間違った形式のレスポンスを送信したり、レスポンスを遅延させたりする機能を提供する。
  • Stateful Behaviour : 状態を定義し、状態によって異なるレスポンスを定義できる機能を提供する。
  • HTTPS : WireMockは、必要に応じ、HTTPSでもリクエストを送信できる機能を提供する。

WireMockの使い方

WireMock機能を使用したアプリケーションを作成するためには、WireMockに対する依存性を追加する必要があります。

Mavenビルドを使用するプロジェクトでは、以下のとおり追加します。

Maven

<dependency>
  <groupId>com.github.tomakehurst</groupId>
  <artifactId>wiremock</artifactId>
  <version>x.x.x</version>
</dependency>

Gradleビルドを使用するプロジェクトでは、以下のとおり追加します。

Gradle

testCompile "com.github.tomakehurst:wiremock:x.x.x"

コード作成中にWireMockのAPIを使用するために、以下の構文をimportします。

import static com.github.tomakehurst.wiremock.client.WireMock.*;

別途のアプリケーションを作成せず、Javaコマンドを使ってWireMockを実行できます。

リポジトリから希望するバージョンのjarファイルをダウンロードし、以下のコマンドを実行します。コマンドに対する付加オプションについては、ヘルプを参考にしてください。

$ java -jar wiremock-standalone-x.x.x.jar

User Interface Testing

UIテスト(UI testing)は、アプリケーションのUIを見つけて相互作用する方法を提供し、各要素の状態と属性を確認できるようにします。モバイルプラットフォーム別に多様なUIテストツールを使用できますが、各プラットフォームの代表的なUIテストツールは以下のとおりです。

  • Android:Espresso、UI Automator、Robolectric、…
  • iOS:XCTest、KIF、…

このうち、LINE広告クライアントモジュールのテストにはEspresso、XCTestを使用します。

Android UI Testing Framework – Espressoの紹介

Googleが開発したUIテストフレームワークで、単一アプリケーションをテストするために使用します。

Android UI Testing Framework – Espressoの使い方

Googleでは、Androidアプリケーションのテストの作成にAndroid Studioを使用することを推奨しています。Android Studioを使用してAndroidプロジェクトを作成すれば、テストのためのsource setsと例題のコードを作成してくれるので、簡単にテストを作成できます。プロジェクト作成時にできるbuild.gradleファイルと例題のテストコードは以下のとおりです。これらのファイルからEspressoの使用に必要な事項を把握することができます。

build.gradle

apply plugin: 'com.android.application'
 
android {
    compileSdkVersion 24
    buildToolsVersion "23.0.2"
    defaultConfig {
        applicationId "com.minhwang.myapplication"
        minSdkVersion 14
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        //Espressoを使用するために追加すべき部分である。
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 
            'proguard-rules.pro'
        }
    }
}
 
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    //Espressoを使用するために追加すべき部分である。
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:design:24.2.1'
    testCompile 'junit:junit:4.12'
}

ExampleInstrumentedTest.java

package com.minhwang.myapplication;
 
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
 
import org.junit.Test;
import org.junit.runner.RunWith;
 
import static org.junit.Assert.*;
 
/**
 * Instrumentation test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();
 
        assertEquals("com.minhwang.myapplication", appContext.getPackageName());
    }
}

テスト中に予期せぬエラーが発生することを防止するには、テストが行われるエミュレータまたはデバイスのアニメーション機能を無効にしておく必要があります。

iOS UI Testing FrameworkーXCTest frameworkの紹介

iOSS UI Testing Frameworkは、WWDC 2015 – UI Testing in Xcodeにて紹介されており、XCTest FrameworkとAccessibilityをコア技術として使用しています。

  • XCTest:XCTestは、UIテストのためのフレームワークを提供し、これをXcodeと統合させました。UIテストの実装は、XCTestを利用して単体テスト実装する既存の方法と同様、テストターゲットを作成してテストクラスと関数を作成して行います。結果の検証にはXCTest assertionsを使用し、Objective-CとSwiftのいずれにも対応します。
  • Accessibility:Accessibilityは、体の不自由なユーザーに豊富なユーザーエクスペリエンスを提供するためのコア技術で、UIに関する有意味な情報を提供します。UIテストは、この情報を利用して行います。

iOSプラットフォームで使用できるテストフレームワークは多種多様です。最初は、KIFというフレームワークを使用する予定でした。しかし、このフレームワークを使用して広告クライアントを動かすと(原因は不明ですが…)正常に動作しなかったため、代替手段として現在のフレームワークを使用することにしました。

iOS UI Testing FrameworkーXCTest frameworkの使い方

iOS UI Testing Frameworkは、Xcode 7、iOS 9以上で使用できます。

プロジェクト作成時にオプションを選択してテストを追加する方法

一番簡単な方法は、プロジェクトを作成する際にUIテストを追加することです。

以下の画面のように、プロジェクト作成オプションを選択する際にInclude UI Tests項目にチェックを入れます。すると、Xcodeプロジェクトが作られる際にUIテストターゲットが作成され、例題のテストを含む実装ファイルが生成されます。

テストターゲットを作成してテストを追加する方法

File>New>Target…をクリックし、iOS UI Testing Bundleテンプレートを選択します。 ターゲット名を入力し、Target to be Testedのドロップダウンメニューからテストしたいアプリのターゲット名を選択します。上記と同様、Xcodeが例題のテストを含む実装ファイルを生成します。

広告クライアントモジュールテストの作成例

以下は、広告クライアントモジュールテストに使われたシナリオの一つです。

Purpose クライアント動作中にDISPLAY POLICYがWEIGHTED_RANDOMからSORTEDに変更された場合、正しく動作するかを確認する。
Preconditions
  1. 広告リストキャッシュが空になっているので、新規でリクエストする。
  2. クライアントがSHOWCASEを初めてリクエストした場合、ordering = WEIGHTED_RANDOM項目とexpireの最小値を含むレスポンスが返される(テスト中に広告が満了するようにするため)。
  3. クライアントが2回目のSHOWCASEをリクエストした場合、ordering = SORTED項目を含むレスポンスが返される。
  4. サーバから受け取った広告リストには、二つの広告が含まれている。一つ目の広告のweight値は0で、二つ目の広告のweight値は100000000である。
  5. クライアントの画面には、一つの広告ビューが表示される。
Steps
  1. 広告ビューが含まれた画面を表示する。
  2. 広告ビューが含まれた画面をリフレッシュする。
Expected results
  1. WEIGHTED_RANDOMで動作しないといけないので、レスポンスに含まれている二つの広告のうち、WEIGHT値が大きい二つ目の広告が画面に表示される必要がある。
  2. SORTEDモードで動作しないといけないので、一つ目の広告が画面に表示される必要がある。
  3. SHOWCASEリクエスト回数は2回である必要がある。

広告サーバのモックを作るためのMappingファイルの作成例

LINEの広告クライアントモジュールは、大きく分けて3種類のURLを使用してリクエストとレスポンスを送受信します。

LINE広告クライアントモジュールは、設定情報を「configuration」URLにリクエストし、画面に表示する広告の情報を「banner」URLにリクエストします。そして、ユーザーアクションと広告ビューで発生したイベントを収集して「events」URLに送ります。

以下のコードは、条件に合致するリクエストを受信したときに返されるレスポンスの例です。このレスポンスには、200ステータスコードと指定されたファイルの内容が含まれます。

ad_configuration.json

{
    "request": {
        "urlPattern": "/line/advertise/v[0-9]+/config",
        "method": "POST"
    },
    "response": {
        "status": 200,
        "bodyFileName": "display_policy/4_1/ad_configuration.json"
    }
}

テストシナリオにおいて、サーバは1回目のSHOWCASEリクエストと2回目のリクエストに対し、それぞれ別の情報がレスポンスに含まれるようにします。以下は、指定された条件に合致するリクエストを初めて受信した場合、指定されたファイルの内容でレスポンスを返し、状態を変更させる例を示したコードです。WireMockのシナリオ機能を使用します。

ad_banner_first.json

{
    "scenarioName": "SHOWCASE",
    "requiredScenarioState": "Started",
    "newScenarioState": "First SHOWCASE",
    "request": {
        "urlPattern": "/line/advertise/v[0-9]+/showcase",
        "method": "POST"
    },
    "response": {
        "status": 200,
        "bodyFileName": "display_policy/4_1/ad_banner_weighted.json"
    }
}

状態が変更された後、サーバは別の内容のレスポンスを返します。

ad_banner_second.json

{
    "scenarioName": "SHOWCASE",
    "requiredScenarioState": "First SHOWCASE",
    "request": {
        "urlPattern": "/line/advertise/v[0-9]+/showcase",
        "method": "POST"
    },
    "response": {
        "status": 200,
        "bodyFileName": "display_policy/4_1/ad_banner_sorted.json"
    }
}

すべてのevent送信に対しては、200ステータスコードのみを含めてレスポンスを返します。

ad_event.json

{
    "request": {
        "urlPattern": "/line/advertise/v[0-9]+/stat",
        "method": "POST"
    },
    "response": {
        "status": 200
    }
}

iOSクライアントのテストコードの作成例

以下は、iOSで広告クライアントモジュールテストを行うために作成したアプリの画面構成です。

iOSでは、すべてのテストは基本的に以下のような流れで行われます。

  1. 各テストに必要な内部キャッシュを設定
  2. テスト画面を選択
  3. テストに必要な動作を実施
  4. UIのAccessibilityによって動作の適合性の有無を判断

キャッシュ設定画面のJSONファイルは、テストプロジェクトにbundle形式で含まれています。各ファイル名をタップするとそのファイルの内容をキャッシュに入力します。

上記のテストシナリオのために作成したテスト関数は以下のとおりです。テスト関数を実行する前に、setUp()関数を使用してテストアプリを起動します。

func testGivenDisplayPolicyChangedToSorted_WhenViewReloaded_ThenShowTheFirstAd() {
        // 各テストに必要なmappingファイルのパスを表すtestStubを設定する。
        let testStub = AdDisplayPolicyTestStub(adStubID: 
        .testGivenDisplayPolicyChangedToSorted_WhenViewReloaded_ThenShowTheFirstAd)
 
        self.adViewTestingHelper!.prepareTest(testStub)
         
        let app = XCUIApplication()
        let tablesQuery = app.tables
         
        // 画面からAd Listを含むテーブルの行をタップする。
        tablesQuery.staticTexts["Ad List"].tap()
        // 画面からClear AD Listを含むテーブルの行をタップし、キャッシュの内容を削除する。
        tablesQuery.staticTexts["Clear AD List"].tap()
        // 前の画面に戻るためにTesting Optionsボタンをタップする。
        app.navigationBars["line_ios_advertise_test_force.AdListCacheView"].
        buttons["Testing Options"].tap()
 
        // テスト画面に入るために画面名を含むテーブルの行をタップする。一つ目の広告ビューを画面に
        表示し、広告のタイトルを広告ビューのAccessibility Labelの値に設定する。
        tablesQuery.staticTexts["Ad Display Policy Testing View"].tap()
        sleep(2)
 
        let firstShowingAd = XCUIApplication().otherElements["lineAdView_1"].label
 
        //広告をリフレッシュするために別画面に移動してから復帰する。
        app.navigationBars["line_ios_advertise_test_force.AdDisplayPolicyView"].
        buttons["Next"].tap()
        app.navigationBars["line_ios_advertise_test_force.EmptyView"].
        children(matching: .button).matching(identifier: "Back").
        element(boundBy: 0).tap()
 
        // SHOWCASEリクエスト回数および表示された広告のタイトルをチェックし、
        正しく動作しているかを確認する。
        let result = self.adViewTestingHelper?.
        getRequestCountFromServerFor(AdRequestURLs.AdBannerUrl.rawValue)
        XCTAssertNil(result?.error)
        XCTAssertEqual(2, result?.count)
        XCTAssertEqual("2_in_first_response", firstShowingAd)
        XCTAssertEqual("1_in_second_response", XCUIApplication().
        otherElements["lineAdView_1"].label)
}

Androidクライアントのテストコードの作成例

iOSのテストコードと類似した流れで動作しますが、使用するAPIが異なるため、詳細な使い方には違いがあります。setUp()でキャッシュを初期化する作業を行います。

@Test
public void testGivenDisplayPolicyChangedToSorted_WhenViewReloaded_ThenShowTheFirstAd() 
throws IOException, JSONException {
    String firstShowingAd;
 
    Given: {
        AdDisplayPolicyTestStub.AdStubIDs stubID = AdDisplayPolicyTestStub.AdStubIDs.
        testGivenDisplayPolicyChangedToSorted_WhenViewReloaded_ThenShowTheFirstAd;
        AdDisplayPolicyTestStub testStub = new AdDisplayPolicyTestStub(stubID);
        this.testingHelper.requestServerToPrepareStub(testStub);
    }
 
    When: {
        this.activity = this.testingHelper.launchActivity();
        SystemClock.sleep(2000);
 
        firstShowingAd = this.activity.getAdTitle();
 
        // Refresh the contents of the advertisement
        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
        @Override
         public void run() {
          InstrumentationRegistry.getInstrumentation().callActivityOnPause(activity);
          InstrumentationRegistry.getInstrumentation().callActivityOnStop(activity);
          InstrumentationRegistry.getInstrumentation().callActivityOnRestart(activity);
          InstrumentationRegistry.getInstrumentation().callActivityOnResume(activity);
            }
        });
 
        SystemClock.sleep(2000);
    }
 
    Then: {
        Assert.assertEquals("2_in_first_response", firstShowingAd);
        Assert.assertEquals("1_in_second_response", this.activity.getAdTitle());
        Assert.assertEquals(2, this.testingHelper.
        getRequestCountFromServerFor(AdRequestURLs.AD_BANNER_URL.getURL()));
    }
}

Continuous Integration

作成したすべてのテストは、決まったスケジュールに沿ってJenkinsで実施されます。なお、テスト結果とカバレッジ分析結果がレポートされます。

まとめ

今回の記事では、LINE Ads Platformのモバイルクライアントのテスト方法をご紹介しました。モバイルクライアントテストツールであるWireMock、そしてAndroidとiOSのテストフレームワークであるEspressoとXCT frameworkの使い方を説明いたしました。また、CIサーバが自動的にレポートするテスト結果とカバレッジ分析結果も共有しました。

作成者の紹介

ファン・ミン(Hwang Min): LINEのSDKを検証するためのテストコードの作成を担当しているテストエンジニアです。