お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

Android のコンポーネント(Activity)の単体テスト3 - 別アクティビティからの結果を取得する

タイトル長いな……。簡潔に言うと onActivityResult のテストをする方法です。

前回の続きになります。

pickles-ochazuke.hatenablog.com

概要

前回と同じ Espresso.Intents を使います。Espresso-Intents については、前回話しているので飛ばします。

onActivityResult は、requestCode, resultCode, インテントデータを受け取ります。この内、resultCode と インテントデータの2つのデータは、別のアクティビティが setResult() で設定しています。この別のアクティビティが設定する部分をスタブ化します(スタブ化は、簡単に言えば、機能を真似するようにする。ということです)。

スタブ化するためには、Intents.intending() とその戻り値である OngoingStubbing のメソッド respondWith() を使います。

developer.android.com

developer.android.com

developer.android.com

次のように使います。

intending(hasComponent(OtherActivity::class.java.name)).respondWith(result)

使い方としては、intending() の引数にスタブ化したい対象が一致する条件を渡し、そのスタブ化の対象が呼ばれたときに渡したい結果を respondWith() に渡します。

上記だと OtherActivity が呼ばれた(OtherActivity が作成された)ときに、そのアクティビティが終了されたものとし、result を onActivityResult() の各引数(resultCode, intent)に渡されます。

上手く説明出来ませんが、実際に使用すると感覚が掴めると思います。

実践では、前回のプロジェクトを使うので、なければこちらの ActivityUnitTestExample_2 ブランチを使うと同じ状態に出来ます。

github.com

実践

今回追加する処理は、前回のプロジェクトを使い、OtherActivity を起動したら挨拶の情報を持ったインテントが返ってくるので、それをテキストに反映させるという処理にします。テストで確認するだけなので OtherActivity 側は何も変更しません。ですので、実際のアプリで OtherActivity は何も返してきません。

まずは、別アクティビティから結果を受け取れるようにします。MainActivity.kt を以下のように変更します。

import android.widget.TextView
...
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode != RESULT_OK) { return }
        if (requestCode != 1) { return }

        findViewById<TextView>(R.id.helloWorld).text = data?.getStringExtra("greeting")
    }

ついでに、startActivity() から startActivityForResult() に変更します。

startActivityForResult(intent, 1)

次にテストを追加します。

import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import androidx.test.espresso.intent.Intents.intending

    @Test
    fun OtherActivityから受け取った結果がテキストに反映されるべき() {
        val intent = Intent().apply {
            this.putExtra("greeting", "Hi World!")
        }

        val result = Instrumentation.ActivityResult(Activity.RESULT_OK, intent)
        intending(hasComponent(OtherActivity::class.java.name)).respondWith(result)

        onView(withId(R.id.button)).perform(click())
        onView(withId(R.id.helloWorld)).check(matches(withText("Hi World!")))
    }

intending() 手前までは、ActivityResult を作成しています。intending() の引数は、スタブ化したい対象の条件を指定しています。そして、respondWith()ActivityResult を渡しています。

https://developer.android.com/reference/android/app/Instrumentation.ActivityResult

スタブ化が完了したら、Espresso で UI を操作し、ビューが期待通りになっているか検証します。デバッグ実行を行い、onStartActivity()onView(withId(R.id.button)).perform(click())ブレークポイントを設定するとが perform() 実行されたあとに onStartActivity() で止まることが確認できます。

以上です。

ここまで行った状態が途中に上げた Github にあるリポジトリActivityUnitTestExample_3 ブランチです。

雑記

GW で調べていたことを放出しました。本当は Todo アプリを作って Android の理解を深めようと思ったのですが、その Todo アプリをテストしようとしたらいろいろエラーを踏んで3,4日潰れました……。まだテストのことは理解が足りていないので間違ったことを書いているかもしれませんが、誰かの助けになれば幸いです(間違っていれば指摘しただけると助かります)。

今年のGWは、技術書読み進めたり、Android のテスト調べたり、これ書いたりと割と充実していました(それでもやり残したことはありますが)。テストのエラー解決できないとき諦めようと思いましたが時間あけて考え直すと解決したので、やっぱり諦めずにいるのは大事だなと思いました(そして適度な休息!)。

あと恋する小惑星を見て地学に興味湧いたので NHK の地学基礎って動画見てるんですがかなり面白くてノートにまとめたりしています。

koiastv.com

www.nhk.or.jp

ちなみに私はジャイアント・インパクト説派です(他の説あんまり知りませんが)。

Android のコンポーネント(Activity)の単体テスト2 - 別アクティビティの起動

一応、前回の続きです。

pickles-ochazuke.hatenablog.com

概要

今回は、テスト対象のアクティビティから別のアクティビティを起動するテストを作成します。

別のアクティビティが起動したかをテストするのですが、単体テストなので別のアクティビティに強く依存したくはありません。例えば、別のアクティビティを起動して、そのビューが期待通りになっているかどうかまでは確認する必要はないと思います。これは、そのアクティビティ(別のアクティビティ)側の単体テストの役割だからです。

アクティビティを起動する場合は、インテントを作成し、startActivity() メソッドにインテントを渡すことで Android 側が処理してくれます。ですので、startActivity() メソッドに期待するインテントが渡されていれば起動できていることにしても良さそうです。

単体テストでは、実機を使うわけではないので、Android のシステムのようにインテントを管理してくれる代わりが必要になります。これは、Espresso では、Intents クラスがしてくれます。Espresso のドキュメントでは用語が紛らわしいためか、Espresso-Intents と表現しています。

developer.android.com

この Intents クラスを初期化していると、startActivity() メソッドに渡されたインテントが Intents クラスに記録されます。その後、Intents のインテント検証用のメソッド(intended() メソッド)を使うことで期待する値になっているか検証できます。

例えば、次のように使います。

Intents.intended(hasComponent(OtherActivity::class.java.name))

intended() メソッドに渡した条件(上記だと OtherActivity クラスの名前を持ったインテントがあるかどうか)を元に Intents が記録しているインテントを検証します。検証した結果、一致したのが一つのときテストが成功します。複数のインテントが一致した場合や、一つも一致しない場合は例外が投げられテストが失敗します。

developer.android.com

実践

プロジェクトの準備

ここからは、実際にテストをしてみます。プロジェクトは、前回のプロジェクトをそのまま使います。

前回の最後の状態のプロジェクトは、Github に上げています。ブランチは、ActivityUnitTestExample_1 です。

github.com

別のアクティビティ起動の実装

まずは、別のアクティビティを起動する処理を実装します。ここでは、ボタンを押したらアクティビティが起動するようにしたいと思います。

別のアクティビティを起動するためには、そのアクティビティが必要になので、OtherActivity という名前で作成しておきます。中身は一切編集しないので、テンプレートは何でもいいですが、ここでは EmptyActivity にしました。

次にボタンを追加します。activity_main.xml にボタンを追加し、id を button とします。

レイアウトにボタンを追加したら MainActivity.kt にボタンの処理を実装します。

import android.widget.Button
import android.content.Intent

...
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            val intent = Intent(this, OtherActivity::class.java)
            startActivityForResult(intent, 1)
        }
    }
...

実装に不安があれば、ここで一度エミュレータや実機で確認しましょう。

テストの追加

次のようにテストを追加します。

import androidx.test.espresso.action.ViewActions.click

import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent

...
    @Test
    fun ボタンを押すとOtherActivityが起動するべき() {
        onView(withId(R.id.button)).perform(click())

        intended(hasComponent(OtherActivity::class.java.name))
    }

Espresso-Intents は拡張機能のため、app/build.gradle に以下を追加します。

dependencies {
  ...
  testImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
  ...
}

Gradle の同期を行い、テストを実行すると java.lang.NullPointerException が発生すると思います。これは、Intents の初期化がされていないのに intended() メソッドを呼んだためです。Intents.init() メソッドを呼ぶと初期化されますが、テストの終了時に Intents.release() を呼ぶ必要があります。

前回、Activity を管理してくれる ActivityScenarioRule というクラスを使いました。これと似たようなものが Intents 用に存在します。IntentsTestRule です。

activityScenarioRule() が書かれた箇所を次のように置き換えます。

import androidx.test.espresso.intent.rule.IntentsTestRule

    @get:Rule
    val rule = IntentsTestRule(MainActivity::class.java)

activityScenarioRule を残していると、アクティビティが二回作成され、テストが上手くいきません。原因は分かっていませんが、おそらく onView() で処理しているアクティビティの対象が activityScenarioRule の方を見ており、その場合、インテントが記録されないためです。

上記を追加したあと、テストを実行すると成功するはずです。

以上です。

ここまでの状態を Github のほうに上げました。ブランチを ActivityUnitTestExample_2 に変更するとその状態になります。

github.com

続きを書きました。

pickles-ochazuke.hatenablog.com

雑記

Kotlin のコードが上手く色づけされないのって何でですかね(対応しているらしいですが)。書き方が間違っているのか、何か設定しないといけないのか。全体的に見た目が気になりだしたのてこれを気にいろいろ弄ってみてもいいのかもしれない。

Android コンポーネント(Activity)の単体テスト

最近はスキレットで肉を焼くのに地味にハマっています。

f:id:pickles-ochazuke:20200508000431j:plain

さて今回は、Android のテストについて書きます。一般的なテストについてはここでは解説しません。

テストのピラミッド

テストは大きく分けて3つの層に分かれます。ピラミッドで表しているのをよく見ます。

developer.android.com

ピラミッドなので、一番下が太く、一番上が細い三角形△になっています。 太さはそのまま全体の中で占める割合を表しています。上から順番に並べると

  • 大規模テスト(UI テスト(E2Eテスト))
  • 中規模テスト(Integration(結合) テスト)
  • 小規模テスト(Unit(単体) テスト)

の順番です。一般的には、上に行くほど本番運用の再現性が高くなりますが、テストを開始してから完了するまでの時間(実行時間)や保守およびデバッグの労力が増えます。 先に挙げたURLのドキュメントを読むと以下のような割合が推奨されています。

一般的には、小規模テスト 70%、中規模テスト 20%、大規模テスト 10% の割合でテストを作成することをおすすめします。

今回作成するテストは、小規模テストにあたる単体テストです。

Android のテスト概要

Android のテストの実行方法は大きく分けて2種類あります。

  • 実機、エミュレータで実行するテスト
  • ローカルマシン内で実行するテスト

これは、先に挙げたピラミッドのどれかに当てはまるわけではありません。各層でどちらのテストも行うことが出来ます。

実機、エミュレータで実行するテスト

実際の使用に近いテストですので、本番運用の再現度が高いです。ただし、実行するまでの時間、テスト実行から完了までの時間が長くなります。

エミュレータは、Android Studio 標準で備えてある Android Emulator を使うのが一般的でしょう。

テストコードは、app/src/androidTest に配置されます。

インストゥルメントテスト、インストゥルメント化テストとも呼ばれ、意味的には実機テストのことだと思います。といっても実機に限らずエミュレータでも行えるので実機に近いテストといった感じでしょうか。

ローカルマシン内で実行するテスト

ローカルマシン、つまり開発しているPC内で行うテストです。テスト対象がAndroid に依存しないコードであれば、一般的な Java, Kotlin のコードをテストするように行えますが、Android フレームワークに依存する場合は、Robolectric のようなフレームワーク(テストフレームワーク?)を利用して行う必要があります。

Robolectric というのは、先と似た話になりますが、Android フレームワークに依存したコードを JVM 上で実行するためのフレームワークです。そのため、実機やエミュレータのテストと比べると再現性は落ちてしまいますが、実行速度は Android に依存しないテストに近い速度で実行できます。

robolectric.org

テストコードは app/src/test に配置されます。

また、Robolectric はテストを行うための API(ライブラリ?)を提供していますが、ここでは Robolectric は実行環境としてだけ使用し、テストコードは、AndroidX Test API を使用します。テストコードを AndroidX Test API に統一すると一つのソースコード単体テストと実機テストの両方でテストできるようになります。ここでは紹介しません(そのやり方を知らないので)。

参考

developer.android.com

Espresso とは

Espresso とは、Android UI テストのためのライブラリです。AndroidX に含まれています。Espresso を使うことで簡潔に書くことができます。同期的に処理が行われるので、信頼性の高いテスト結果が得られます

Espresso は4つの主なコンポーネントがあります。

  • Espresso
  • ViewMatchers
  • ViewActions
  • ViewAssertions

Espresso

onViewonData メソッドを使ってビューとやり取りするためのエントリポイントです。

ViewMatchers

Matcher<? super View> インタフェースを実装するオブジェクトのコンポーネントです。onView メソッドにこのコンポーネントを一つ以上渡して現在のビュー階層からビューを特定します。一意に特定できない場合は、例外が投げられます。

ViewActions

ViewInteraction.perform メソッドに渡すことができる ViewAction オブジェクトのコレクションです。

ViewAssertions

ViewInteraction.check メソッドに渡すことができる ViewAssertion オブジェクトのコレクションです。アサーションが失敗した場合、例外が投げられます。

Hamcrest

Espresso のコンポーネントではないですが、Espresso を使うときによく一緒に使われます。マッチャーオブジェクトを記述するためのフレームワークです。

hamcrest.org

Espresso を使った UI のテストコードは以下のようになります。

Espresso.onView(ViewMatchers.withId(R.id.helloWorld))
    .check(ViewAssertions.matches(ViewMatchers.withText("Hello World!")))

ViewMatchers.withId() でビューを特定する条件を指定し、onView() に渡します。次に check() の引数に指定した条件が指定のビューに一致するか検証します。ここでは、特定したビューが "Hello World!"という文字列を持っているか検証しています。

実践

ここからは、プロジェクトを作成し、実際にテストコードを書いてみます。

Android Studio のバージョンは、3.6.3 です。

プロジェクトの作成

次のような設定をして、プロジェクトを作成します。

  • Project Template
    • EmptyActivity
  • Configure
    • Name
      • ComponentTestExample2
    • Language
      • Kotlin
    • Minimum SDK
    • Use legacy android.support libraries
      • チェック入れない

環境

今回実行する環境です。

  • Android Gradle Plugin Version
    • 3.6.3
  • Gradle Version
    • 5.6.4
  • Kotlin Version
    • 1.3.71
  • Compile Sdk Version
    • 29
  • BuildT Tools Version
    • 29.0.0

プロジェクトを作成したら、 app/src/test/ExampleUnitTest.kt を開き、テストを実行します。ソースコード内の行数の右隣に緑の三角が横に向いたようなアイコンがあると思います。それをクリックすることで実行できます。

developer.android.com

テストの作成

先に実行したテストは、プロジェクトの内容と関係ないテストなので、MainActivity をテストするためのファイルを作成します。

テスト対象のファイルを開き(MainActivity.kt)、開いたら Ctrl+Shift+TWindowsの場合)を押します。すると図のようなメニューが開くので、Create New Test をクリックします。

f:id:pickles-ochazuke:20200507231427p:plain

f:id:pickles-ochazuke:20200507231517p:plain

Testing libraryJUnit4 にします。

f:id:pickles-ochazuke:20200507231539p:plain

OK をクリックするとChoose Destination Directory ウィンドウが開くので、/app/src/test/... の方を選択します。OK をクリックすると指定したディレクトリにテストファイルが作成されます。作成されたMainActivityTest.kt ファイルの中身は以下のようになっていました。

package com.example.ochadukebiyori.componenttestexample2

import org.junit.Assert.*

class MainActivityTest

テストの追加

MainActivity のレイアウトファイルを見ると画面の真ん中に "Hello World!" という文字が表示されているので、これをテストしたいと思います。レイアウトファイルを開いたついでに、"Hello World!" を表示しているTextViewhelloWorldという id を指定します。

テストは、@Test が付与されたメソッドがテストになります。MainActivityTest.kt を以下のようにします。

package ...
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    @Test
    fun 起動したらHelloWorldが表示されるべき() {
        onView(withId(R.id.helloWorld))
            .check(matches(withText("Hello World!")))
    }
}

@RunWith でテストランナーを指定します。AndroidJUnit4Android の標準的なテストランナーです。テストのメソッド名はわかりやすさを重視するため日本語で書いています。テスト内容は、テスト対象のビューを特定する id(helloWorld)がテキストに "Hello World!" という文字列を持っているか検証しています。

Espresso や Robolectric のようなライブラリは、デフォルトでは使えないので、build.gradle を編集し、モジュールを追加します。

dependencies {
    ...
    testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    testImplementation 'androidx.test.ext:junit-ktx:1.1.1'
    testImplementation 'org.robolectric:robolectric:4.3.1'
    ...
}

実行すると次のようなエラーが出ると思います(ワーニングも出ますがそちらは無視します)。

java.lang.RuntimeException: No activities found. Did you forget to launch the activity by calling getActivity() or startActivitySync or similar?

これは、Activity が作成されていないのが原因です。次のコードを追加することでアクティビティが作成されます。

import androidx.test.core.app.launchActivity

...
class MainActivityTest {
  val scenario = launchActivity<MainActivity>()
  ...
}

launchActivity() を使うためには、モジュールを追加する必要があるので、build.gradle に次のように追加します。

dependencies {
  ...
  testImplementation 'androidx.test:core-ktx:1.2.0'
  ...
}

ActivityScenario は、アクティビティのライフサイクルのための API を提供してくれるモジュールです。

developer.android.com

実行すると次のようなエラーが出るかも知れません(あるいは、実行する前に赤い波線で示されているかも)

Cannot inline bytecode built with JVM target 1.7 into bytecode that  is being built with JVM target 1.6

これは、launchActivity()JDK1.7 以上じゃないと使えないためです。次のように build.gradle にターゲットを指定します。

android {
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

ついでに Robolectric でリソースを使えるようにするために以下も行います。

リソースを使用する場合は、次のように includeAndroidResources オプションを有効にします。基本的にビューの特定は id で行うため、必須になります。しかし、Android Studio 3.4 以上はデフォルトで有効になっている?ような記述が見られるので必要ないかもしれません。ですが、自分の環境では指定していない場合、エラーが出たので追加しています。

android {
  ...
  testOptions {
      unitTests {
          includeAndroidResources = true
      }
  }
}

gradle を同期して、実行すると次のようなエラーが出るかもしれません。

java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8)

これは、自分はあまり理解出来ていないのですが、 Robolectric は、ターゲットバージョンに合わせて環境が作られます。今回だと Android SDK 29 です。Robolectric を 29 で動かす場合は、 JDK が 9 以上である必要があるそうです。ですが、今は JDK 8 で動かしているため上記のようなエラーが出ます。

ここでは、Robolectric でのターゲットバージョンを 28 にすることで回避します。テストの再現性が落ちるので良くないですが、練習のため簡単な方法で回避します(本当は JDK 9 以上にするのが良いと思います)。

app/src/test/resources ディレクトリを追加し、そのディレクトリ内に robolectric.properties ファイルを追加します。そして、以下を追記します。

sdk=28

再度実行するとテストが通るはずです。

ActivityScenario はデバイスの状態を自動でクリーンアップ(初期化)しないため、テスト終了後もアクティビティが実行され続ける可能性があります。ですので、テストの最後には次のように明示的にクローズします。

...
  @Test
  fun 起動したらHelloWorldが表示されるべき() {
      onView(withId(R.id.helloWorld))
          .check(matches(withText("Hello World!")))
      scenario.close()
  }
...

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

テストコードのリファクタリング

今回の目的は達成しましたが、途中で出てきた ActivityScenario は閉じることを忘れるとテストコードに予期せぬバグが発生する可能性があるため、それを避けるように修正します。

ActivityScenario は、テスト終了後もアクティビティを実行し続ける可能性があると書きました。このActivityScenario を管理してくれるクラスがいます。それは ActivityScenarioRule です(昔は、ActivityTestRuleでしたが、今は非推奨になっています)。

developer.android.com

このクラスを次のように書いておくと、テストの開始前と終了後にアクティビティの作成と開放を行ってくれます。

...
import org.junit.Rule
import androidx.test.ext.junit.rules.activityScenarioRule
...
class MainActivityTest {
    @get:Rule
    var rule= activityScenarioRule<MainActivity>()
...
}

先程追加した ActivityScenario の処理は不要になったので、削除しています。

テストを実行して、通るか確認しましょう。

ActivityScenarioRule は、ActivityScenario を持っているので次のように参照する事ができます。

val scenario = rule.scenario

アクティビティは自動で終了してくれますが、手動でアクティビティを終了しても問題ありません。

developer.android.com

お疲れさまでした。以上です。

ここまでの状態を Github の方に上げています。ブランチは ActivityUnitTestExample_1 です。

github.com

続きを書きました。

pickles-ochazuke.hatenablog.com

雑記

FGO アニメ見終わりました。マーリンって名が出るたびになんか聞き覚えあるなぁと思っていたのですが、ドラクエ5のまほうつかいが仲間になったときの名前でした。通りで魔法使いっぽい名前してるなと思いました。

技術書典8(技術書典 応援祭)で同人誌を初販売した話

技術書典8 で初めて本を出しました。その時に困ったこと、こうしておけば良かったことを記録として残します。

技術書典とは

techbookfest.org

技術書典とは、技術書だけの同人誌即売会です。2020年3月で8回目になります。今回は、新型コロナウィルス(COVID-19)の影響で物理での販売は中止になりました。また、今までは1日だけのイベントだったみたいですが、今回行われていれば、初の2日に跨いだイベントでした。

物理での販売は中止になりましたが、導入を検討していたらしいオンラインでの販売(技術書典 応援祭)が即席で可能になりました。これは、3/7 ~ 4/5 まで行われました。

サークル応募するときに気をつけること

開催日は、2/29(土), 3/1(日)の予定でした。受付を開始したのは、11/5(火)です。年に2, 3回行われているので、受付を開始していなくても早めに動いて良さそうです。

受付時点で、以下の点は決まったほうが良いです。印刷関係は物理本を頒布する場合の話です。

  • 印刷所
  • 参加人数
  • ジャンル
  • 書く内容
  • 頒布数
  • ページ数
  • 販売形態(物理だけか電子だけか両方か)

印刷所

印刷所によってスケジュールが異なります。安くなる期間、高くなる期間は把握しておきたいです。

また、搬送方法なども把握しておきます(直接搬送可能か)。 印刷所によって出来る印刷方法が異なりますのでどんな本にしたいかも決める必要があります。 オフセット印刷か、オンデマンド印刷か。中綴じか、無線綴じか。表紙はカラーか、全カラーか、紙の質はどうするのか、など何が出来て、どうしたいかを決める必要があります。ここが決まると印刷所が絞られます。

同人誌を買ったことがある人は、本の見た目や質感が良かった本がどこで印刷されているか見てみましょう。大抵、最後のページに記載されていると思います。印刷所によっては、紙の質のサンプルを送って頂けるので、早めに確認しておきたいです。

参加人数

自分含めて最低二人参加出来るのが望ましいです。トイレに行けなくなってしまいます。コミュ障の人にとっては、人脈が必要になるので難易度高いですが……。

ジャンル

参加受付時に、記載する必要があります。これによって配置場所が決まります。ジャンル自体は後で変更が可能ですが島のジャンルに合わなくなるので最初で決めれるなら決めたいです。

書く内容

初心者向けか、そうではない人向けか。誰向けの本なのかまたは、どんな本を書きたいのかを決めます。

これも後で変更可能ですが、サークルが一般公開された場合、参加者が事前に情報を集めるので、それまでには確定しておきたいです。ジャンルと合わせて一般公開までに決めるべき項目です。

頒布数

参加受付時に記載する必要があります。これとページ数によって(もちろんそれ以外も)配置場所を参考にするようなので一度決めたら変えないほうが良いでしょう。初めてなら20~30でしょうか。印刷所との料金と自分の財産とも相談する必要があります。

ページ数

頒布数と同じです。また本を書く時のスケジュールに影響します。内容によりますが、20p~50pぐらいなら書けると思います。

販売形態(物理だけか電子だけか両方か)

電子か、物理か、両方か。電子だけなら印刷所を決める必要がないので楽ですが、頒布方法を考える必要があります。物理の場合は印刷所やスケジュール、どんな本にするかなど考えることが多いです。

書くことに集中したい場合は、電子本のほうが良いかもしれません。ただ物理の場合は、いざという時に電子本に移行しやすいです。

自分は物理本を予定していましたが、中止になったので電子に移行になりました。ですが、印刷のスケジュールなどを考慮しなくてよくなったので良かったです。初めては電子がオススメです。

書く時に気をつけること

  • 書く時間を作る(決める)
  • 定期的に pdf に出力する
  • 定期的に校正する(pdfで確認する)
  • 強調や参照の書き方は最初からしておく

書く時間を作る(決める)

まとめてやるよりも定期的に書くほうが書きやすいです。また、書いてると調べたりする必要が出てくるので思ったよりも進まないです。

校正をしたり、自分の書く内容(書きたい内容)があっているかなどの確認や変更が出てくる可能性があるので、とにかく早く書いたほうが良いです。

早く書き上がると印刷料金も安くなりますしね!

定期的に pdf に出力する

校正するためにも、pdf に出力して自分が期待する見た目になっているか確認するためにも定期的に出力します。最初は、動作確認のために書く前にまず出力したほうがいいでしょう。

定期的に校正する(pdfで確認する)

校正するときは、pdf で読んだほうが良いです。なるべく本の形に近い形で確認するほうが発見が多いと思います。タブレットでペンを使って校正出来ると良いです。私は、iPad のファイルアプリからpdfを開いて、ペンを使って校正していました。これだと隙間時間に確認できて良いです。

また、校正した内容は早めに反映させます。

強調や参照の書き方は最初からしておく

強調や参照、表の作成など環境によって書き方は違うと思いますが、後で置き換えるのではなく最初からしておいたほうが良いです。その書き方を最初に身に着けておけば探し直す手間がなくなります(また校正で気づけます)。

また、書き方はなるべく早く決めて統一出来たほうが良いです。例えば半角のときは前後に半角スペースを入れるのか、コードの書き方はどうするのか、ですます調かである調かなど、表記の揺れは本の質を落とすことになります。

印刷(電子版の場合)

今回は物理的な印刷はしていないので、電子版の場合の注意点です。

  • 出力した pdf は、様々な媒体で確認する
  • ファイル名を決める
  • ファイルサイズを確認する

出力した pdf は、様々な媒体で確認する

今回の一番の失敗ポイントです。

pdf の確認は、ブラウザでしか確認していませんでした。MacWindows それぞれで確認していましたが、販売開始してから WindowsAcrobat Reader では開けないことが運営の方から連絡がありました。 Mac で pdf を編集していたのですが、そのやり方がまずく、編集し直して販売しているファイルを入れ替えました。この間に一冊売れていたので本当に申し訳ないことをしてしました(再ダウンロードすればいいのだけど、購入した方に連絡する方法がない)。

ブラウザだけでなく、AdobeAcrobat Reader でも確認しましょう(この二種類確認しておけば問題ないはず)。

ちなみに Windows のブラウザ(古い方の Edge)では問題なく表示されていました(これによって問題ないと思っていた)。

ファイル名を決める

これは販売完了してから気づいたのですが、最初に決めたてきとうなファイル名で販売していました。ファイル名にバージョン情報などあると良いと思います。

ファイルサイズを確認する

表紙を画像にする場合、ファイルサイズが大きくなる可能性があります。なるべく小さくしたい人は、確認しておいたほうがいいでしょう。

次回気をつけたいこと

今回でいろいろ課題が見えました。次回には改善したいです。

  • 物理本で頒布する
  • 印刷所、印刷方法を早めに決める(応募するときに決める)
  • ポスターを作る
  • 図、絵を入れる
  • Re:VIEW の使い方を身につける
  • 自分の書きたいことを全部詰め込めるようにする(必要ないものは削る)

全部行えるかは分かりませんが、良い本だと思ってもらえるように頑張ろうと思います。

ちなみに出した本は、技術書典 応援祭だけでなく Booth でも販売しています。

techbookfest.org

ochadukebiyori.booth.pm

今回出した本を次回、物理本で販売出来たらいいなと思っているので、それまでにもっと良い本になるようにどこかで更新したいと思っています。それを告知出来る手段も用意したいですね。

また次回サークル参加が受かればそのときは、新しい本を書こうと思っていますので、買って頂けるように頑張ります。

販売実績

3/15 に販売開始して、全部で13冊売れました。

f:id:pickles-ochazuke:20200412164414p:plain

途中参加にも関わらず思ったよりも売れました。購入して下さった方には感謝しています。同時に期待に沿えた内容になっているか不安ではありますが、これは徐々に改善しようと思っています。

最初の週あたりの販売数で止まるかなと思っていたのですが、終わりの方で8冊売れているので、傾向としてはイベント終了間際が一番売れやすいんですかね。

ここには載せておらず、データが少ないので確かなことは言えないですが、夜から深夜(17時~25時)あたりによく売れています。朝に買う人は少ない印象です。平日は深夜で、休日は昼から夜に売れています。平日は深夜に購入する人が多いのが驚きでした。運営の方が定期的に特集をしていたのでその影響もあるのかもしれません。あまり把握出来てませんが。

正直、物理で売る場合はこんなに売れない気がしています。また、このデータを参考に次回の頒布数を決めたいと思います。

ここらへんのデータが簡単に取得できると嬉しいなぁ。

終わりに

初めての同人活動でしたが、総合としてはやって良かったです。購入して頂いた方に満足してもらえたかは分かりませんが、自分にしてはよく出来たんじゃないかと思います(反省点も多々あります)。また、本を出す以外にも販売する楽しさも味わえました。自分で作った物がお金になるのってかなり面白いです。

次回はもっと良いものを作って売れるように頑張ります。

最後に、こんな状況の中短い時間でオンライン販売を行えるようにしてくれた技術書典運営の方々にはとても感謝しています。ありがとうございました。

表紙を描いてくれた友達にも感謝しています。ありがとう!

WebdriverIO を TypeScript で実行する

WebdriverIO とは

ブラウザをNode.js から自動で操作し、テストを行うことができるJavaScriptフレームワークです。

webdriver.io

www.lambdatest.com

環境構築

Node.js はインストールされているものとします。

$ node -v
v13.2.0
$ npm -v
6.13.1

ディレクトリを作成し、初期化します。

$ mkdir webdriverio-typescript
$ cd webdriverio-typescript
$ npm init -y

WebdriverIO を追加します。

$ npm install --save-dev @wdio/cli
...
$ npx wdio --version
6.0.5

wdio の環境構築

wdio の環境を作成するために以下のコマンドを実行します。すると質問形式で設定を行えます。

$ npx wdio config
=========================
WDIO Configuration Helper
=========================

? Where should your tests be launched? local
? Where is your automation backend located? On my local machine
? Which framework do you want to use? jasmine
? Do you want to run WebdriverIO commands synchronous or asynchronous? sync
? Where are your test specs located? ./test/specs/**/*.js
? Which reporter do you want to use? spec
? Do you want to add a service to your test setup? selenium-standalone
? What is the base url? http://localhost

上記のように答えました。テストフレームワークは Jasmine を使います。また、WebdriverIO のライブラリは同期的に行うほうが書くのが楽なため、 sync にしています。

selenium-standalone を選択した場合

この手順は必要ないかもしれません。

webdriver.io

wdio.conf.js を以下のように編集します。

exports.config = {
    ...
    services: [
        ['selenium-standalone', {
            logPath: 'logs',
            installArgs: {
                drivers: {
                    chrome: { version: '80.0.3987.149' },
                    firefox: { version: '0.26.0' }
                }
            },
            args: {
                drivers: {
                    chrome: { version: '80.0.3987.149' },
                    firefox: { version: '0.26.0' }
                }
            },
        }]
    ],
    ...
}

chrome, firefox のバージョンはブラウザによってインストールしているブラウザによって変わると思います。


ここでは Chrome を使うため、browserName の値を chrome に変更します。

exports.config = {
    capabilities: [{
    ...
        browserName: 'chrome',
    ...
    }],
}

テストファイルを置くためのディレクトリを作成し、テストファイルを作成します。

$ mkdir -p test/specs/
$ touch ./test/specs/trial-test.js

試しに実行します。

$ npx wdio run wdio.conf.js

Execution of 1 spec files started at 2020-03-29T07:41:40.623Z

2020-03-29T07:41:40.728Z INFO @wdio/cli:launcher: Run onPrepare hook
2020-03-29T07:41:43.158Z INFO @wdio/cli:launcher: Run onWorkerStart hook
2020-03-29T07:41:43.159Z INFO @wdio/local-runner: Start worker 0-0 with arg: run,wdio.conf.js
[0-0] 2020-03-29T07:41:43.494Z INFO @wdio/local-runner: Run worker command: run
[0-0] 2020-03-29T07:41:43.499Z INFO webdriverio: Initiate new session using the ./protocol-stub protocol
2020-03-29T07:41:43.666Z INFO @wdio/cli: [0-0] SKIPPED in firefox - /test/specs/trial-test.js
2020-03-29T07:41:43.666Z INFO @wdio/cli:launcher: Run onComplete hook
2020-03-29T07:41:43.666Z INFO @wdio/selenium-standalone-service: shutting down all browsers

Spec Files:  0 passed, 1 skipped, 1 total (100% completed) in 00:00:03

2020-03-29T07:41:43.667Z INFO @wdio/local-runner: Shutting down spawned worker
2020-03-29T07:41:43.922Z INFO @wdio/local-runner: Waiting for 0 to shut down gracefully
2020-03-29T07:41:43.922Z INFO @wdio/local-runner: shutting down

trial-test.js にテストコードを書きます。

describe("Trial Test", function () {
    it("WebdriverIO のトップページのヘッダタイトルは WebdriverIO であるべき ", function () {
        browser.url("https://webdriver.io/");
        expect(browser.getTitle()).toContain("WebdriverIO");
    });
});

再度実行して、テストが通ることを確認します。設定ファイル名が wdio.conf.js であれば、以下のように指定する必要はありません。

$ npx wdio

...
[Chrome 19.2.0 darwin #0-0] 1 passing (1.5s)

Spec Files:  1 passed, 1 total (100% completed) in 00:00:08

2020-03-29T07:50:52.203Z INFO @wdio/local-runner: Shutting down spawned worker
2020-03-29T07:50:52.454Z INFO @wdio/local-runner: Waiting for 0 to shut down gracefully
2020-03-29T07:50:52.454Z INFO @wdio/local-runner: shutting down

TypeScript に対応する

JavaScript で対応できたので、TypeScript で実行できるようにします。

webdriver.io

まずは TypeScript の環境を作ります。

$ npm install typescript
$ npx tsc --version
Version 3.8.3
$ npx tsc --init
message TS6071: Successfully created a tsconfig.json file.

環境に合わせて tsconfig.json を編集します。wdio と Jasmine のコードをコンパイルしてもらうために、以下は必須です。

{
  "compilerOptions": {
    "types": [
        "@wdio/sync",
        "jasmine"
    ],
  },
  "exclude": [
    "node_modules"
  ],
}

コードを書くときにJasmine を認識してもらうため @types/jasmine を追加します。

npm install --save-dev @types/jasmine

@wdio/syncwdio を追加したときに一緒に含まれているので自分で追加する必要はありません。

次に TypeScrip のファイルを作成します。

$ touch trial-test.ts

trial-test.ts にテストコードを書きます。

describe("Trial Test", () => {
    it("WebdriverIO のトップページのヘッダタイトルは WebdriverIO であるべき ", () => {
        browser.url("https://webdriver.io/");
        
        expect(browser.getTitle()).toContain("WebdriverIO");
    });
});

コンパイルした結果の出力先を変更するために tsconfig.json を以下のように変更します。先に作った trial-test.js は上書きされるので、置いておきたい人は退避するか、出力先を変更してください(出力先を変更した場合、 wdio.conf.js の設定も変更する必要があるかもしれません) 。

{
  "compilerOptions": {
    ...
    "outDir": "./test/specs",
    ...
  }
}

コンパイルが通るか確認します。

$ npx tsc

test/specs/trial-test.js が変わっているか確認します。自分の環境では以下のようになっていました。

"use strict";
describe("Trial Test", function () {
    it("WebdriverIO のトップページのヘッダタイトルは WebdriverIO であるべき ", function () {
        browser.url("https://webdriver.io/");
        expect(browser.getTitle()).toContain("WebdriverIO");
    });
});

実行できるか確認します。問題なければ前回と同じ結果が出ます。

$ npx wdio

コンパイルせずにテストを実行する

最後に、 tscコンパイルするのは面倒なので、コンパイルしなくても通るようにします(内部でコンパイルしてくれている?)。

webdriver.io

ts-nodetsconfig-paths を追加します。

$ npm install --save-dev ts-node tsconfig-paths

+ ts-node@8.8.1
+ tsconfig-paths@3.9.0

TypeScript ファイルを実行するために wdio.conf.js を開き、jasmineNodeOptsrequirests-node/register を追加します。

exports.config = {
    jasmineNodeOpts: {
        ...
        requires: ['ts-node/register']
        ...
    },
}

TypeScript ファイルを見つけるために wdio.conf.js を開き、specs の値を ./test/specs/**/*.ts に変更します。

exports.config = {
    specs: [
        './test/specs/**/*.ts'
    ],
}

コンパイルしたファイルで実行していないか確認するために、test/specs/trial-test.js を削除します。代わりにtrial-test.tstest/specs/ に移動させます。

実行して確認します。

$ npx wdio

問題なければ今までと同じ結果が表示されるはずです。