テストフレームワークのSpekを使えるようにする

最初に

Kotlinで書かれているテストフレームワークSpekをAndroidのテストでも使えるかどうかという検証を行いました。

結論

結論としては、普通に使おうとするとRobolectricなどと併用することができないため、今のところ使うのは辛い感じです。

Spekとは

JetBrains社が開発しているKotlinでテストが記述できるようになるテスティングフレームワークです。1
これを導入すると、初期化, テスト対象の処理実行, アサーションと改行などで区切っていた部分をDSLで区分けして記述できるようになります。

@Test
public void testCalculateTaxRate() {
  TaxRateCalculator calculator = new TaxRateCalculator();

  Int value = calculator.calculateRate(200, 10);

  assertEquals(300,value);
}
class SimpleTest : Spek({
  describe("a calculator") {
    val calculator = SampleCalculator()

    it("should return the result of adding the first number to the second number") {
      val sum = calculator.sum(2, 4)
      assertEquals(6, sum)
    }

    it("should return the result of subtracting the second number from the first number") {
      val subtract = calculator.subtract(4, 2)
      assertEquals(2, subtract)
    }
  }
})

導入

使えるようんいするまでのGradleでの設定方法について記述していきます。
Kotlinの導入は終わっているものとします。

Gradleの設定

SpekはJUnit5をベースとしたフレームワークであるのでJUnit4で動かすには違う設定が必要です。2

公式HPには記載されていなかったのですが、下記で試しているバージョンでは、kotlin-test-junitが必要でした

KOTLIN_VERSION=1.1.3

SPEK_VERSION=1.1.2

JUNIT_PLATFORM_VERSION=1.0.0-M4

repositories {
  maven { url "http://dl.bintray.com/jetbrains/spek" }
}

dependencies {
  testCompile ("org.jetbrains.spek:spek-api:${SPEK_VERSION}") {
    exclude group: 'org.jetbrains.kotlin'
  }
  testCompile ("org.jetbrains.spek:spek-junit-platform-engine:${SPEK_VERSION}") {
    exclude group: 'org.junit.platform'
    exclude group: 'org.jetbrains.kotlin'
  }
  testCompile "org.jetbrains.kotlin:kotlin-test-junit:$KOTLIN_VERSION"
  testCompile "org.junit.platform:junit-platform-launcher:${JUNIT_PLATFORM_VERSION}"
  testCompile "org.junit.platform:junit-platform-runner:${JUNIT_PLATFORM_VERSION}"
}

テストコードの記述

JUnit4でSpekのテストを実行するためには、@RunWith(JUnitPlatform::class)のアノテーションが必要です。

そのために、TestRunnerを指定するようなテストライブラリは使えなくなります。
例えば、RobolectricTestRunnerを指定する必要があるRobolectricです。

@RunWith(JUnitPlatform::class)
object SampleCalculatorTest : Spek({
  given("a caluculator") {
    val caluculator = SampleCalculator()
    on("addition") {
      val result = calculator.add(2, 4);
      it("should return 6") {
        assertEquals(6, result)
      }
    }
    on("substraction") {
      val sum = calculator.substract(2, 4);
      it("should return -2") {
        assertEquals(-2, result)
      }
    }
  }
})

用意されているブロック

  • given
    • テストの内容を説明します。givenを使うときは、onと同時に使い、onで処理の細かい条件等を説明しテストを実行する形が良さそうです。
  • on
    • 実行する処理を説明します。テスト対象のメソッド単位に用意するのが良さそうです。
  • it
    • 実行する(した)処理の期待する振る舞いなどを説明します。データのバリエーションを用意して複数の確認を行う場合はそれぞれにitを定義するのが良さそうです。
  • describe
    • テストの内容を説明します。また、複数のdescribeを並べて複数のテストを行えます。

まとめ

現時点では公式でRobolectricとの併用などをサポートしていないようなので使うことは難しそうですが、使えるようになるとテストコードがより読みやすくかけるようになりそうです。
次回までにCustomTestRunnerを用意できたらと思っています。


  1. https://github.com/JetBrains/spek 
  2. http://spekframework.org/docs/latest/#setting-up-legacy 

Androidのリモートビルドを行う

概要・結論

sys1yagiさんのブログ1などで興味を持って、Androidのbuildはマシンスペックで殴ろうというお話。結果として、開発で使っているMacbook Airでビルドするよりとても早い。

最新のGradle Pluginからは爆速っぽい2ですが、試せてないのと試そうとするとなんか罠に嵌りそうなので。

マシンスペック ビルド時間
CPU: 1.3GHz Intel(R) Core(TM) i5, Mem: 4GB 4min程度
CPU: 3.4GHz Intel(R) Core(TM) i7-2600 Mem:24GB 40sec程度(ファイルの同期時間:10sec, ビルド:30sec)

こんなひと向け

  • 開発で使っているPCのスペックが低くAndroidのbuildに1min以上かかっている
  • AWS, GCPなどで十分なマシンスペックのインスタンスを用いてでもビルド時間を短くしたい

リモートビルドの仕組み

ただ単純に高スペックなマシンを用意して、

  • そのマシンにビルドに必要なファイルを同期する
  • そのマシン上でbuild
  • その成果物をローカルマシンに同期する

ってことをしている。

Androidのビルドができる高スペックなマシン

AWSとかにインスタンスを立てることもなく、5年前に買ったPCのスペックで十分だったのでAWSとかには用意していない。
そのマシンにはDockerを起動していて、その上に今回のビルド環境のコンテナを作成した。
Dockerfileは下記のようなもの

FROM ubuntu:16.04

EXPOSE 22

ENV DEBIAN_FRONTEND noninteractive

ENV ANDROID_SDK_URL http://dl.google.com/android/android-sdk_r24.4.1-linux.tgz
ENV ANDROID_HOME /usr/local/android-sdk-linux
ENV ANDROID_SDK /usr/local/android-sdk-linux
ENV PATH ${ANDROID_HOME}/tools:$ANDROID_HOME/platform-tools:$PATH

# Install Java.
RUN \
  apt-get update && \
  apt-get install -yq curl software-properties-common python-software-properties && \
  echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
  add-apt-repository -y ppa:webupd8team/java && \
  apt-get update && \
  apt-get install -y oracle-java8-installer && \
  rm -rf /var/lib/apt/lists/* && \
  rm -rf /var/cache/oracle-jdk8-installer

# Define commonly used JAVA_HOME variable
ENV JAVA_HOME /usr/lib/jvm/java-8-oracle

# Install for Android
RUN dpkg --add-architecture i386 && \
  apt-get update && \
  apt-get install -yq openssh-server libc6:i386 libstdc++6:i386 zlib1g:i386 libncurses5:i386 && \
  apt-get clean

# Download and untar SDK
RUN curl -L "${ANDROID_SDK_URL}" | tar --no-same-owner -xz -C /usr/local

# Install Android SDK components
# License Id: android-sdk-license-ed0d0a5b
ENV ANDROID_COMPONENTS platform-tools,build-tools-25,android-25
# License Id: android-sdk-license-5be876d5
ENV GOOGLE_COMPONENTS extra-android-m2repository,extra-google-m2repository

RUN echo y | android update sdk --no-ui --all --filter "${ANDROID_COMPONENTS}"
RUN echo y | android update sdk --no-ui --all --filter "${GOOGLE_COMPONENTS}"

リモートマシンへのファイル同期、リモートマシン上でのコマンド実行

参考にしている方たちのブログにあるpythonのスクリプトを用いている3。pythonとか普段使わないので、中身をすこし見てみると他のことにも転用できそうな感じでとても良いものだった。
pythonにはFabric4というssh経由でリモートマシン上でのコマンド実行を行うためのライブラリがあり、このライブラリを使ってファイル同期、リモートマシン上でのbuild実行を行う。
git push時のhookでテストだけリモートでやるなどってこともスクリプトに追加して行えるようにしてみたりしている。


  1. http://sys1yagi.hatenablog.com/entry/2016/11/13/162400, http://qiita.com/shikajiro/items/6bc52c3f4c0b6dcbbfc3 
  2. http://tools.android.com/tech-docs/new-build-system/2-5-alpha-gradle-plugin 
  3. https://gist.github.com/sys1yagi/1f395190061a8d48db5e30ae160b4b99 
  4. http://www.fabfile.org/ 

CoordinatorLayoutのBehaviorを自作する

この記事の概要

上記のBehaviorでは達成できない動きでも、自身でBehaviorを実装するとこんなこともできるようになるよっていうことをBehaviorの種類に合わせて紹介したいと思います。

Behaviorの種別

Behaviorを実装する上での方針です。下記のどの表現を行うかでBehaviorの実装の仕方を変えると良いです。
この記事での成果物は、GitHub[https://github.com/ttymsd/coordinator-behaviors](https://github.com/ttymsd/coordinator-behaviors)にまとめてあります。

1. CoordinatorLayout直下のViewとの相対位置を保つ

実装するもの

  • Behavior#onLayoutChild
    • CoordinatorLayout上での初期位置を指定します。
  • Behavior#layoutDependsOn
    • View(child)がどのView(dependency)との相対位置とするか決定する。
  • onDependentViewChanged
    • layoutDependsOnで対象としたViewがCoordinatorLayout上で位置が変わったときにコールバックされ、Behaviorを持つViewの位置の再レイアウトができる。

2. NestedScrollChildが実装されているViewのスクロール量に合わせて動かす

実装するもの

  • Behavior#onLayoutChild
    • CoordinatorLayout上での初期位置を指定します。
  • Behavior#onStartNestedScroll
  • Behavior#onStopNestedScroll
  • Behavior#onNestedPreFling
  • Behavior#onNestedFling
  • Behavior#onNestedPreScroll
  • Behavior#onNestedScroll
  • Behavior#onNestedScrollAccepted
    • CoordinatorLayout直下のNestedScollViewで発生したScrollEventが通知されます。

上記のメソッドを実現したい表現に合わせてOverrideしてBehaviorが設定されているViewを動かします。
Ex.BottomNavigationBehavior

3. TouchEventを処理する(Ex.CoordinatorLayout内でDragなどで移動させる)

  • Behavior#onLayoutChild
    • CoordinatorLayout上での初期位置を指定します。
  • Behavior#onInterceptTouchEvent
    • CoordinatorLayoutで発生したonInterceptTouchEventの処理が委譲されます。trueを返すと引き続きTouchEventの処理をこのBehaviorでできます。
  • Behavior#onInterceptTouchEvent
    • CoordinatorLayoutで発生しているTouchEventが委譲されているので、TouchEventを処理してViewを動かしたりします。

まとめ

いままで、CustomViewを作ってchildView等を把握してDrag処理等に合わせてLayoutしていたものが、Layout処理だけがBehaviorに切り出されることで
使い回しがしやすくなった感じを受けるのと、動かしたいView毎にBehaviorを用意することで1画面上でいろいろな動きをさせられるようになったと思う。