ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Android アプリバンドル (Android App Bundle) について学んだ

2018 年の Google I/O でも発表があった *1 Android アプリバンドル (Android App Bundle)。 Android Studio 3.2 を使っているとアプリのビルドで App Bundle を選べるようになっていたりするし何となく存在は知っていたけどちゃんと調べてはいなかった。

developer.android.com

Instant アプリを試しに作ってみようとしたら Android アプリバンドルの知識が必要になったので、この機会にちゃんと調べてみることにした。 とりあえずコードラボの内容をさーっと見たのでまとめておく。

Android アプリバンドルについて

  • Android アプリバンドル (Android App Bundle) : Google Play へアップロードするための新しい形式。 アプリのコンパイルされたコードとリソースをすべて含むが、APK 生成や署名は Google Play に任される。
  • 動的配信 (Dynamic Delivery) : Google Play における新しいアプリ提供のモデル。 各ユーザー端末に最適化された APK を生成して提供するためにアプリバンドルが使用される *2
  • 動的機能モジュール (dynamic feature modules) : この種のモジュールをアプリのプロジェクトに追加し、アプリバンドルに含めることで、必要になってから動的配信を通じてダウンロードされるアプリの機能を作ることができる。 2018 年 11 月 11 日時点ではまだベータ版。

動的配信 (Dynamic Delivery) は、分割 APK 機構 (split APK mechanism) *3 を基礎としている。 分割 APK により、Google Play は大きなアプリを小さなパッケージに分解できる。 分割 APK の種類は 3 種類。

  • Base APK : 他の分割 APK から使用されるコードやリソースを含み、アプリの基本機能を提供する。 ユーザーがアプリをダウンロードする際には常にこの APK が含まれる。
  • Configuration APKs : 指定された端末構成のためのネイティブライブラリとリソースのみを含む。 ロケールや画面密度、CPU アーキテクチャといったものに依存する APK コンテンツを最適化するため。
  • Dynamic feature APKs : アプリが最初にインストールされたときには必要でないが、あとからダウンロードされて使われるかもしれない機能を含む。

これらの APK は Google Play がビルドして提供してくれる。 Android 4.4 (API level 20) 以下の端末向けには、Google Play が自動で端末構成に最適化された単一 APK を提供してくれる。

Android Studio 3.2 でアプリバンドルを生成する

プロジェクト自体は普通に作成すればそれで良い。 (Dynamic feature module を作らないなら、プロジェクト構造は通常の app ディレクトリ下にアプリ全体のコードやリソースを含める形式で良い。)

Android Studio で、「Build」 メニューから 「Build Bundle(s)」 を選んだり、「Generate Signed Bundle or APK」 の 「Android App Bundle」 を選んだりすることでアプリバンドルを生成できる。

コードラボには、app/build.gradle の android { } ブロックに下記の内容を追加する必要があるようなことが書かれているが、「Add support for Dynamic Delivery  |  Android Developers」 を見る限りは Android Studio 3.2 の正式版の段階ではデフォルトで有効になっているようで、下記記述はしなくても良さそう。

bundle {
   language {
       enableSplit = true
   }
   density {
       enableSplit = true
   }
   abi {
       enableSplit = true
   }
}

アプリバンドルの確認

アプリバンドルを確認する方法は 2 つ。

  • ローカルで bundletool コマンドラインツールを使用する。
  • Play Console にバンドルをアップロードし、内部テストトラック (internal test track) を使って Google Play を通して確認。

Bundletool というのは、Gradle や Android StudioGoogle PlayAndroid アプリバンドルをビルドしたり、アプリバンドルから種々の APK に変換したりするのに使用しているツール。 コマンドラインツールとしても使用できる。 「Releases · google/bundletool · GitHub」 で JAR 形式で配布されている。 使用方法はコードラボにも書かれているし、「bundletool  |  Android Developers」 などにも書かれている。

アプリバンドルを Google Play にアップロードする

Google Play へのアップロードで気を付ける必要があるのは 「Google Play アプリ署名」 を使用する必要があるということぐらいで、それ以外には特に難しいところはなかった。

App Bundle を使用するアプリには Google Play アプリ署名が必要です。アプリ署名を有効にした後の流れは次のとおりです。

アプリ署名鍵を管理する - Play Console ヘルプ

おまけ : アプリバンドルの instant app サポート

2017 年から instant apps というものは存在していた *4 が、アプリバンドルによってより簡単に実現できるようになった。

ドキュメントを見る限りは簡単なのだけど、自分の場合は動作確認にかなりてこずってしまった。 また今度まとめる。

*1:Google Developers Japan: Google I/O 2018:Android の新機能 など参照。

*2:それぞれの端末に必要なコードやリソースだけがダウンロードされる。

*3:Android 5.0 (API level 21) 以降で使用可能

*4:【インストールせずにすぐ実行できる!】Android Instant Apps を使ってみた – PSYENCE:MEDIA など。

MockK と JMockit の組み合わせで AttachNotSupportedException 例外が発生することがあるっぽい

Kotlin で Mockito を使うのが辛くなってきた *1 ので、「よーしパパ MockK 入れちゃうぞー」 と言って MockK 1.8.12 を導入したのだけど、その結果テストを実行すると以下のような初期化エラーが発生するようになってしまった。

java.lang.ExceptionInInitializerError
	at MyTest.<init>(MyTest.kt:101)
Caused by: java.lang.IllegalStateException: Error during attachment using: net.bytebuddy.agent.ByteBuddyAgent$AttachmentProvider$Compound@718207
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:384)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:358)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:326)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:312)
	at io.mockk.proxy.jvm.JvmMockKAgentFactory.initInstrumentation(JvmMockKAgentFactory.kt:122)
	at io.mockk.proxy.jvm.JvmMockKAgentFactory.init(JvmMockKAgentFactory.kt:31)
	at io.mockk.impl.JvmMockKGateway.<init>(JvmMockKGateway.kt:45)
	at io.mockk.impl.JvmMockKGateway.<clinit>(JvmMockKGateway.kt:163)
	... 22 more
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at net.bytebuddy.agent.Attacher.install(Attacher.java:84)
	at net.bytebuddy.agent.ByteBuddyAgent.install(ByteBuddyAgent.java:379)
	... 29 more
Caused by: com.sun.tools.attach.AttachNotSupportedException: no providers installed
	at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
	... 35 more

スタックトレースの一番下を見ると com.sun.tools.attach.VirtualMachine なので、JDK の内部でなんかなってるのかなー、と最初は思っていたのだけど、IDE で辿っていくと VirtualMachine クラスは JMockit に含まれているものだということがわかった。 使っていた JMockit が 1.22 と少し古いものだったので、最新の 1.43 を使うようにしたら無事エラーは発生しなくなった。

Mockito でも同様のエラーが発生することがある模様。

ちなみに com.sun.tools.attach.VirtualMachine は、JVM にアタッチするための API に含まれるものらしい。

JaCoCo の Java 10 対応はバージョン 0.8.1 から

Gradle 4.6 で Gradle の JaCoCo プラグインを使用しているプロジェクトをビルドすると下記のエラーが発生した。 Java 環境は OpenJDK 10。

java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:510)
        at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:522)
Caused by: java.lang.RuntimeException: Class java/lang/UnknownError could not be instrumented.
        at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:139)
        at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:100)
        at org.jacoco.agent.rt.internal_290345e.PreMain.createRuntime(PreMain.java:55)
        at org.jacoco.agent.rt.internal_290345e.PreMain.premain(PreMain.java:47)
        ... 6 more
Caused by: java.lang.NoSuchFieldException: $jacocoAccess
        at java.base/java.lang.Class.getField(Class.java:1958)
        at org.jacoco.agent.rt.internal_290345e.core.runtime.ModifiedSystemClassRuntime.createFor(ModifiedSystemClassRuntime.java:137)
FATAL ERROR in native method: processing of -javaagent failed
        ... 9 more

java.lang.NoSuchFieldException: $jacocoAccess』 と出ているので JaCoCo 関連っぽいということで調べたところ、JaCoCo の JDK 10 対応は JaCoCo 0.8.1 からだった。

Gradle 4.6 時点での JaCoCo プラグインでは、デフォルトで JaCoCo 0.8.0 を使うようになっているので、JDK 10 で使うには JaCoCo のバージョンを指定してやる必要がある。

jacoco {
    toolVersion = '0.8.2'
}

ちなみに Gradle 4.10.2 時点ではデフォルトは JaCoCo 0.8.1 になっている。

Azure Pipelines で Android アプリの CI をやってみてる

最近 Microsoft から発表された Azure DevOps。 Visual Studio Team Foundation (VSTF) をリブランドしたものだそう。

VSTF のときに試しのプロジェクトを 1 個作ってそのまま放置していたのだけど、せっかくなのでこの機会に触ってみることにした。 まずは CI/CD サービスの Azure Pipelines を使ってみてる。

azure.microsoft.com

やってみた

単純なビルド

GitHubホスティングしている Android プロジェクトの CI として、単に ./gradlew build するぐらいのものを構成するだけならすごく簡単。

Azure DevOps で新しいプロジェクトを作成して、その中の 「Pipelines」 の 「Builds」 を開くと新しいビルドパイプラインの構成を行う画面が出てくる。 Web 上でぽちぽちするとビルドパイプラインの設定が作られてそのまま GitHub の pull request にしてくれた。

ただ、Gradle のビルドファイルだけを見て 「Gradle プロジェクトだ」 って判断されるみたいで、初期の構成は Android 向けではなくて普通の Gradle プロジェクト向けになってしまっている。 なので、「Build Android apps with Azure Pipelines or TFS - Azure Pipelines & TFS | Microsoft Docs」 を見ながら自分で構成をいじる必要がある。

pool:
  vmImage: 'macOS-10.13'
steps:
- task: Gradle@2
  inputs:
    workingDirectory: ''
    gradleWrapperFile: 'gradlew'
    gradleOptions: '-Xmx3072m'
    publishJUnitResults: true
    testResultsFiles: '**/TEST-*.xml'
    tasks: 'build'

『The Android Emulator is currently available only on the Hosted macOS agent.』 とのことで、主には VM image を MacOS にする必要がある *1

自動ビルドの設定

私の場合、初期状態だと GitHub からの web hook が無効になっていた *2。 ビルドパイプラインの設定 (YAML ファイルじゃなくて web 側) の中に 「Triggers」 ってのがあるので、そこで web hook を有効にするとコミットごとなどの CI 実行を設定できる。

テストカバレッジの Codecov へのアップロード

Codecov にテストカバレッジをアップロードするにはコマンドを実行する必要がある。 Bash タスクも用意されているので、簡単に実現できる。

steps:
- task: Gradle@2
  (中略)
- task: Bash@3
  inputs:
    targetType: 'inline'
    script: 'bash <(curl -s https://codecov.io/bash)'
    workingDirectory: ''
  env:
    'CODECOV_TOKEN': '$(codecovToken)'

codecovToken は自分で設定したビルドパイプラインの secret variable で、環境変数として使うにはこのように明示的にマッピングしてやる必要がある。 最初 env プロパティの部分を inputs の中に入れてしまっていて、YAML パースエラーが発生してちょっとはまってた。


雑感

まだビルド実行とカバレッジのアップロードぐらいしかやっていないけど、それぐらいなら (ちょっとハマった箇所もあったけど) 素直に構成できた。 ビルドパイプライン単体で他の CI/CD ツールと比較しての強みはまだ見えてないのだけど、Azure DevOps として他のサービスとの連携がやりやすい点は強みっぽい。

ライブラリなどのキャッシュ周りとか、ビルド環境を準備するのが大変な場合にどうするのか (CircleCI では Docker コンテナ内でのビルドという解決策が提示されたが、Azure Pipelines の方はどうなってるんだろう) というあたりは気になっている。 (まだ調べられていない。)

*1:MacOS エージェントでしか Android Emulator は使えない、と言っていて Android Emulator 以外の Android SDK への言及はないのだけど、少なくとも Ubuntu 18.04 には Android SDK の準備はなさそうだった。

*2:設定画面に、web hook が無効です、みたいなエラーメッセージが出ていたので、もしかしたら普通は有効になっているものなのかもしれない。

JerseyTest で Servlet のリソースを扱う JAX-RS 部品のユニットテストを行う方法

要約

前提知識 : ServletJAX-RS

Jakarta EE (旧 Java EE) における HTTP サーバーアプリケーションのための API として有名なもの *1ServletJAX-RS がある。 これらは依存関係にはなく、Servlet 単体でも JAX-RS 単体でも使用できる。

一方で、JAX-RSServlet コンテナ内で実行し、JAX-RS コンポーネントServletコンポーネントをインジェクトするというような使い方もできる。

In a product that also supports the Servlet specification, implementations MUST support JAX-RS applications that are packaged as a Web application. See Section 2.3.2 for more information Web application packaging.

JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environments - 11.2.1 Servlets)

The @Context annotation can be used to indicate a dependency on a Servlet-defined resource. A Servlet-based implementation MUST support injection of the following Servlet-defined types: ServletConfig, ServletContext, HttpServletRequest and HttpServletResponse.

JAX-RS: Java™ API for RESTful Web Services version 2.1 (11. Environment - 11.1 Servlet Container)

Servlet のリソースを扱う JAX-RS コンポーネントユニットテストする方法 (JerseyTest を使用)

JAX-RS コンポーネントユニットテストする方法は様々あるだろうが、個人的には JAX-RS の参照実装である Jersey のテストフレームワークを使う方法に親しみがある。

Jersey のテストフレームワークJerseyTest というクラスを提供しており、これを継承してテストクラスを作ると簡単に JAX-RS コンポーネントのテストを記述できる。

public class SimpleTest extends JerseyTest {
    // SimpleResource という JAX-RS リソースをテストしたい場合は、
    // それを ResourceConfig に渡してテスト用の Application を作ればよい。
    @Override
    protected Application configure() {
        return new ResourceConfig(SimpleResource.class);
    }

    // テストメソッド 
    @Test
    public void test() {
        final String responseContent = target("simple").request().get(String.class);
        assertEquals("Hello World!", responseContent);
    }
}

問題 : 単に JerseyTest を継承してテストクラスを定義するだけだと Servlet リソースが扱われない

上のようなテストコードを書いた場合、基本的には問題なくテストが動く。 しかし、SimpleResourceServlet のリソースを扱っている場合 (@ContextHttpServletRequest をインジェクトしている場合など) には、期待通りに動かない。

なぜなら、(おそらく) デフォルトで選択されるテストコンテナは Servlet をサポートしておらず、Servlet のリソースのインジェクトが行われないからである。

解決策 : Servlet をサポートしているテストコンテナを利用する

テストコンテナの決まり方については、ドキュメントに下記のように書かれている。

A test container is selected based on various inputs. JerseyTest#getTestContainerFactory() is always executed, so if you override it and provide your own version of TestContainerFactory, nothing else will be considered. Setting a system variable TestProperties#CONTAINER_FACTORY has similar effect. This way you may defer the decision on which containers you want to run your tests from the compile time to the test execution time. Default implementation of TestContainerFactory looks for container factories on classpath. If more than one instance is found and there is a Grizzly test container factory among them, it will be used; if not, a warning will be logged and the first found factory will be instantiated.

Chapter 26. Jersey Test Framework

一番簡単な方法は JerseyTest#getTestContainerFactory() をオーバーライドすることだろう。

Servlet をサポートしているテストコンテナもドキュメントに書かれている。

Second factory is GrizzlyWebTestContainerFactory that is Servlet-based and supports Servlet deployment context for tested applications. This factory can be useful when testing more complex Servlet-based application deployments.

Chapter 26. Jersey Test Framework

GrizzlyWebTestContainerFactory を使えばよい。 ちなみに GrizzlyWebTestContainerFactoryDeploymentContext として ServletDeploymentContext を求めるので、その点は注意が必要である。

ということで、下記のようなテストクラスを書けば Servlet のリソースを扱う JAX-RSユニットテストを記述できる。

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.ServletDeploymentContext;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.glassfish.jersey.test.spi.TestContainerException;
import org.glassfish.jersey.test.spi.TestContainerFactory;

public class CookieSessionResponseFilterTest extends JerseyTest {

    private TestContainerFactory testContainerFactory;

    /**
     * テスト用のコンテナを生成するメソッドをオーバーライド。
     *
     * ここで {@link GrizzlyWebTestContainerFactory} を指定することで、サーブレットベースのアプリケーションのテストが可能となる。
     */
    @Override
    protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
        // こういう感じで同じインスタンスを返す必要があるのかはよくわからないが
        // JerseyTest の実装がそういう感じになっているのでそれに合わせている。
        if (testContainerFactory == null) {
            testContainerFactory = new GrizzlyWebTestContainerFactory();
        }
        return testContainerFactory;
    }

    /**
     * テスト用のコンテナにデプロイする内容。
     *
     * {@link GrizzlyWebTestContainerFactory} を使うためには {@link ServletDeploymentContext} を返す必要がある。
     */
    @Override
    protected ServletDeploymentContext configureDeployment() {
        ServletContainer jaxRsServlet = ;
        return ServletDeploymentContext
                // JAX-RS コンポーネントは `ServletContainer` に包んでやる。
                .forServlet(new ServletContainer(configure()))
                // Servlet フィルタの追加も可能。
                .addFilter(TestServletFilter.class, TestServletFilter.class.getSimpleName())
                .build();
    }

    // SimpleResource という JAX-RS リソースをテストしたい場合は、
    // それを ResourceConfig に渡してテスト用の Application を作ればよい。
    // {@link #configureDeployment()} をオーバーライドしたらこのメソッドを
    // オーバーライドしなくても良いはず (configureDeployment 内に書けばよいはず) だが、
    // 一応オーバーライドして {@link #configureDeployment()} から呼ぶようにした。
    @Override
    protected ResourceConfig configure() {
        return new ResourceConfig(SimpleResource.class);
    }

    // テストメソッド 
    @Test
    public void test() {
        final String responseContent = target("simple").request().get(String.class);
        assertEquals("Hello World!", responseContent);
    }
}

自分でテストコンテナのファクトリを書いても良い

上ではもともと Jersey のテストフレームワークに用意されている GrizzlyWebTestContainerFactory を利用したが、独自に TestContainerFactory を実装しても良い。 サーブレットを複数含むテストを書きたい場合などにはそうする必要がありそうである。 (そもそもそういうテストを書く必要がある設計にすべきではないという気もするが。)

JerseyTestでHttpServletRequestを使いたい - Chonaso's Commentary』 が参考になる。 (実際に自分で書いたらわりと手を入れる必要はあったが。)

おわり

まとめると、下記のとおり。

  • Jersey のテストフレームワークが便利。
  • Servlet のリソースも含めてテストしたい場合には GrizzlyWebTestContainerFactory を使用すると良い。

とはいえ、JAX-RSServlet のリソースを扱う必要がある場面は非常に限定的であるはずで、JAX-RSAPI だけで完結できる場面ではそうすべきである。 その方がテストも書きやすいし移植性も高い。 複数の API 仕様の知識 (JAX-RS の知識も Servelt の知識も必要) を求めることもなくなるので、開発者に対しても優しいはずである。

仕事で触っているコードには JAX-RS の中で Servlet のリソースを扱っているものがわりとあるのだが、少しずつ JAX-RSAPI に置き換えていきたい。

*1:これ以外にあるのかどうかは知らない。