LINE 광고 SDK를 사용한 테스트 자동화

이번 포스팅에선 LINE 플랫폼이 제공하는 광고 클라이언트 모듈을 테스트하는 데 사용한 방법을 소개하고자 합니다. LINE의 광고 클라이언트 모듈은 모바일과 웹에서 모두 사용할 수 있으며 여기서는 모바일 클라이언트 테스트 방법을 설명합니다.

LINE Ads Platform 개요

LINE의 광고 플랫폼은 아래와 같이 구조가 단순합니다. 서버-클라이언트 간에는 다양한 프로토콜로 통신이 가능하며, 이번 테스트에서는 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 공식 웹사이트에 따르면 WireMock은 HTTP 기반의 API를 위한 시뮬레이터이며 서비스 가상화 도구나 모의 서버로 고려할 만하다고 소개하고 있습니다.

WireMock이 제공하는 기능은 아래와 같습니다.

  • Stubbing: 미리 정의한 매칭 정보를 바탕으로 요청에 대한 HTTP 응답을 정의하고 적절한 응답을 반환하는 기능을 제공합니다.
  • Verifying: WireMock은 수신한 모든 요청 정보를 기억하며 이를 통해 특정 요청에 대한 수신여부 및 상세정보를 확인하는 기능을 제공합니다.
  • Request Matching: URL, HTTP Method, 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를 찾아 상호작용하는 방법을 제공하며, 이를 통해 각 요소의 상태 및 속성을 확인할 수 있습니다. 모바일 플랫폼별로 다양한 UI 테스팅 도구를 사용할 수 있습니다. 각 플랫폼별 대표적인 UI 테스팅 도구는 아래와 같습니다.

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

이들 중 LINE 광고 클라이언트 모듈 테스트에는 Espresso와 XCTest를 사용합니다.

Android UI Testing Framework – Espresso 소개

구글이 개발한 UI 테스팅 프레임워크로, 단일 애플리케이션을 테스트하기 위해 사용합니다.

Android UI Testing Framework – Espresso 사용법

구글에서는 안드로이드 애플리케이션 테스트를 작성하기 위해 Android Studio를 사용하기를 권장합니다. Android Studio를 사용해서 프로젝트를 생성하면 테스트를 위한 source set와 예제 코드가 함께 생성되기 때문에 쉽게 테스트 작성을 시작할 수 있습니다. 프로젝트 생성 시 만들어지는 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 소개

iOS UI 테스팅 프레임워크는 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 사용법

iOS UI 테스팅 프레임워크는 Xcode 7, iOS 9 이상에서 사용할 수 있습니다.

프로젝트 생성 시 옵션 선택으로 사용하는 방법

가장 쉬운 방법은 프로젝트를 생성할 때 UI Test를 포함하는 것입니다.

아래 화면과 같이 프로젝트 생성 옵션을 선택할 때 Include UI Tests를 선택합니다. 이 옵션을 선택하면 Xcode 프로젝트가 생성될 때 UI Test Target을 생성하고 예제 테스트를 포함한 구현 파일을 생성합니다.

테스트 타겟 생성을 통한 테스트 추가 방법

File > New > Target… 메뉴를 선택하고 iOS UI Testing Bundle 템플릿을 선택합니다. 타겟의 이름을 입력하고, Target to be Tested에서 테스트하고자 하는 app의 타겟 이름을 선택합니다. 위와 동일하게 Xcode가 예제 테스트를 포함한 구현 파일을 생성하여 포함시킵니다.

광고 클라이언트 모듈 테스트 작성 예

다음은 광고 클라이언트 모듈 테스트에 사용된 시나리오 중 하나입니다.

Purpose 클라이언트 동작 중, DISPLAY POLICY가 WEIGHTED_RANDOM에서 SORTED로 변경되었을 때 올바르게 동작하는지 확인한다.
Preconditions
  1. 광고 목록 캐시가 비어 있어 새로 요청해야 한다.
  2. 클라이언트가 처음 SHOWCASE를 요청했을 때, ordering = WEIGHTED_RANDOM 항목과 가장 작은 expire 값이 포함된 응답을 받는다(테스트 도중 광고가 만료될 수 있도록).
  3. 클라이언트가 두 번째 SHOWCASE를 요청했을 때, ordering = SORTED 항목을 포함한 응답을 받는다.
  4. 서버로부터 받은 광고 목록에 두 개의 광고가 포함되어 있다. 첫 번째 광고의 weight 값은 “0”이며, 두 번째 광고의 weight 값은 “100000000”이다.
  5. 클라이언트의 화면에 하나의 광고 뷰가 포함된다.
Steps
  1. 광고 뷰가 포함된 화면을 보여준다.
  2. 광고 뷰를 포함한 화면을 새로 고친다.
Expected results
  1. WEIGHTED_RANDOM으로 동작해야 하므로, 응답에 포함된 두 개의 광고 중 WEIGHT 값이 큰, 두 번째 광고의 내용이 화면에 표시되어야 한다.
  2. SORTED 모드로 동작해야 하므로, 응답에 포함된 첫 번째 광고의 내용이 화면에 표시되어야 한다.
  3. SHOWCASE 요청 횟수는 2번이어야 한다.

광고 서버 Mocking을 위한 Mapping file 작성 예

LINE 광고 클라이언트 모듈은 크게 세 종류의 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"
    }
}

테스트 시나리오에서 서버는 첫 번째 SHOWCASE 요청과 두 번째 SHOWCASE 요청에 다른 정보를 응답에 포함시킵니다. 아래 파일은 명시된 조건에 부합하는 요청을 처음 받았을 때, 명시된 파일의 내용으로 응답을 하고 상태를 변경시키는 예입니다. 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 file의 경로를 나타내는 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와 XCTest framework 사용법을 알아보았습니다. 또한 CI 서버를 통해 자동으로 리포트한 테스트 결과와 커버리지 분석 결과도 볼 수 있었습니다.

저자 소개

황민(Hwang Min): Test Engineer로, LINE의 SDK를 검증하기 위한, 테스트 코드 작성 업무를 담당하고 있습니다.