よりぬき「Androidテスト全書」さん
TL;DR
かねてより執筆中であった「Androidテスト全書」をついに出版しました。
我が国のAndroid史に残るほど良い本に仕上がったと思います。Androidのテストのみにフォーカスした本は日本では類を見ないと思いますが、2018年時点でのUIテストとCI/CDの実践的な知見まで含めると、世界でもここまでまとまった本はないんじゃないでしょうか1。 まだテストがなくこれから増やして行きたい現場や、新人教育に時間を割けない会社など、ぜひこの本を買ってAndroidのテストにチャレンジしてみてください!
それから、これはとても重要なことなのですが、出資者のみなさまで内容にご満足いただけた方はぜひ紹介コードを使ってお知り合いに薦めてあげてください。キャッシュバックがあります2。 これはまだプロジェクトが成立するかどうかも分からない段階から我々を支援してくださったみなさまだけの権利です。みなさまのご支援なしに我々の執筆はありえませんでした。心から感謝します。
さて、このエントリでは自分の担当した章からいくつか見どころを紹介したり、ボツネタを供養したり、思い出話を焚き上げたりしようと思います。お付き合いください。
Overview
購入を検討してくださっている方のためにざっと全体を俯瞰します。 すべてを紹介したいのですが、エントリが長くなりすぎるため自分の担当章のみに留めます。その他の章はそれぞれの著者が解説エントリをかいてくれるでしょう。
第1章「テスト入門」
本書はLocal Unit Test3からUIテストを含むInstrumented Test4まで、単に「テスト」と言っても広大な範囲をカバーしています。 したがって1章ではまず本書における用語の定義と整理をしています。
それからテストを書くことが品質を上げるのみならず、結果的に開発スピードまで上がってしまう話をしています。 スタートアップなんかでありがちなんですが、「スピード優先で」テストを書かない現場もあると思うんですけど、これは個人的には疑問に感じています。 なのでボスが上記のようなことを言い出したら「Androidテスト全書にはテストを書いたほうが早くできるって書いてましたけど」とか言ってやってください。
第2章「ユニットテスト実践入門」
生まれて始めてAndroidのユニットテストを書く人のために、JUnit 4とHamcrestを使ってテストを書く方法をごくごく簡潔に解説しました。 それから、AssertJを使ったアサーションも簡単に解説しています。なぜTruthやAssertK、Expektじゃないんだ?というような話にも触れています。
2章の目玉は「テストダブル」です。次のようなテスト対象クラスを用意して、「スタブ」「モック」「スパイ」を自作しながらそれぞれの使い分けを解説しています。
class WeatherForecast { val satellite = Satellite() fun shouldBringUmbrella(): Boolean { val weather = satellite.getWeather() return when (weather) { Weather.SUNNY, Weather.CLOUDY -> false Weather.RAINY -> true } } }
自作したテストダブルは結局Mockitoで置き換えるのですが、どうしてこれをわざわざ自作してまでみっちり解説したかというと、テストダブルの正しい定義と使い方を身につけていれば仮にMockitoが廃れた世界線でもこの知識は活かせると信じたからです。
本章の草案ではテストダブルの説明そのものも意図的にMockitoの実装に寄せていたのですが、アーリーアクセスのKazuCocoa氏のご指摘を受けてxUnit Patterns.comのTest Doubleに準拠するように書き直し、サンプルコードも全面的に改めました。とても良い章になったと思います。
第3章「ユニットテスト応用編」
前章が意図的にAndroidフレームワークに依存しないモジュールのユニットテストだったのに対し、本章ではまずAndroidフレームワークのコードを利用したモジュールのテスト方法を解説しました。それから、これまでの内容を総合して「現場で使えるテクニック」をいくつか厳選して収録しました。 普段開発していて自分の中に溜め込んだAndroidのテストの知見というのはそれこそ大小さまざまに数えきれないほどあり、すべてを解説するのは無理というものです。したがって自分でうんうんうなったり、Twitterでアンケートを実施したりしてテーマを絞りました。そのうち2つを紹介します。
ひとつは非同期処理のテストです。ExecutorService
を使った次のようなモジュールのユニットテストの方法を解説しました。
class StringFetcher { fun fetch(): String { Thread.sleep(1000L) return "foo" } } class AsyncStringFetcher(val fetcher: StringFetcher) { val executor: ExecutorService = Executors.newCachedThreadPool() fun fetchAsync(onSuccess: (value: String) -> Unit, onFailure: (error: Throwable) -> Unit) { executor.submit { try { val value = fetcher.fetch() onSuccess(value) } catch (error: Throwable) { onFailure(error) } } } }
ここでは CountDownLatch
を使った方法に留まらず、もう一歩先に進めたアプローチも紹介しています。
非同期処理ライブラリも栄枯盛衰が激しく、何かひとつに絞って解説しても廃れたらどうしようもないというのは明白です。従ってここでも「非同期処理はどうしてテストがしづらくて、どういう考え方を身につけていればこの先新しい非同期処理ライブラリが出てきても工夫してテストが書けるのか?」ということを意識しながら書きました。詳しくは本編で!
もうひとつは「テストのないプロジェクトにテストを導入する」と題して、テストコードがない現場でいかにも目にするような悲しみに包まれたJavaのコードを題材にして、少しずつ改良しながらテストを書いていく節です。次のようなクラスを少しずつ変えながらテストを書いていきます。
public class LegacyCode { private LocalDataFetcher localDataFetcher = new LocalDataFetcher(); private RemoteDataFetcher remoteDataFetcher = new RemoteDataFetcher(); void loadData(String param, Context context, Callback<OldData> callback) { OldData result; if (NetworkUtils.isOnline(context)) { result = remoteDataFetcher.fetch(param); } else { result = localDataFetcher.fetch(param); } callback.onSuccess(result); } }
どうでしょうか、ワクワク(げっそり)しませんか?こちらもぜひ本編を読んでいただきたいです。
その他の章は他の著者の方に譲るとして、本書は特に日本語でまとまった知見の少ないUIテストのノウハウががっつり3章に渡って収録されているかなり貴重な書籍です。 ぜひお求めいただけると嬉しいです。
この本に書かなかったこと
先程も書きましたが、この本を書くにあたって収録しきれなかったネタは山程あります。 アサーションにしてもモックにしても応用編にしても、構想段階や執筆段階で倍ぐらいあったものを削りに削っていまの形になりました。著者としてはコンパクトな文量ですべてを収められなかった悔しさというのはどうしてもあります。
例えば、 AsyncTask
や AsyncTaskLoader
のテスト方法は敢えて収録しませんでした。理由は本当にいくつもあるのですが、これらのモジュールがかつて程は用いられてないことや、 Activity
と密結合していてどの章のどの節で解説すべきなのか、それも Robolectric
を使って Local Unit Test
で無理やり解説すべきなのか、 IdlingResource
を駆使して Instrumented Teset
として解説すべきなのか…ね、悩ましいでしょう。それよりかは、非同期処理の本質に迫るような解説にするに留めました。
それから非同期処理でもちゃんとエラーを通知できるJUnit Rulesなんていうのも最初書いてたんですが、これもカットしました。ここで供養しましょうか。
class MultiThreadFail : TestRule { val errorRef = AtomicReference<AssertionError>() fun fail(message: String) { errorRef.set(AssertionError(message)) } override fun apply(base: Statement?, description: Description?): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { errorRef.set(null) base?.evaluate() errorRef.get()?.let { throw it } } } } } class TestClass { val onSuccess: (value: String) -> Unit = { _ -> rule.fail("ERROR") } val onFailue: (error: Throwable) -> Unit = { _ -> rule.fail("ERROR") } @get:Rule val rule = MultiThreadFail() /* ... */ }
こんな風にしておけば非同期処理のコールバックの中からでもアサーションを失敗させられます。 ただ、テストケースからルールにアクセスするのはキモイとか色々査読コメントをいただき、収録しませんでした。
他にも、実際に解説したライブラリひとつ取っても、便利ではあるんだけどマニアックすぎるメソッドは解説を断念したものも多数あります。
Parcelable
のテストも ThreeTenABP
のテストも SharedPreferences
のテストもみんなみーんな書きたかった。本を書くというのは本当に難しい。
思い出話
さて、この本は元々僕が書きたい!と言って書くことになりました。 なので他に僕よりも優れた著者陣が多数参加してくださったにも関わらず、筆頭著者として本当に色々好き勝手やらせていただきました。 たとえば、ある著者とのやり取りでは「めっちゃ内容は良いんですが前戯が長いのでヌける部分を手っ取り早くください!」みたいなクソ失礼な注文を付けたりしたのですが、最終的に素晴らしい玉稿5をあげてくださいました。
優秀な著者のみなさま、編集長のひつじさんとPEAKSの永野さんには感謝してもしきれません。 特にひつじさんの編集は本当にびっくりしました。空間認識能力が違いすぎるとでも言うのでしょうか、他人の書いた膨大な長さの原稿を一瞬で全体像を把握して適切な長さに圧縮する力は脱帽としか言いようがありません。凄いものを見ました。本当にありがとうございます。何もかも、とても良い思い出になりました。
そして何よりも、プロジェクトをサポートしてくださったみなさんに厚く御礼申し上げます! このエントリを読んで興味を持ってくださった方、きっと損はさせません!買ってください!宜しくお願いいたします🙏
最後に、やっぱり妻と子供にはお礼を言わないわけにはいきません。どれだけの休日を犠牲にしたかわかりません。 異国の地に引っ越してきたばかりで孤独と不安だったことでしょう。とても申し訳ない。
4歳の長女とはいつも一緒に寝ていたのだが、僕が執筆のために毎夜書斎にこもるようになってからは、書斎にあるゲストベッドに毎晩長女も泊まりに来るようになりました。毎晩しばらく一緒にベッドに入ってやって、おしゃべりして、背中をポリポリして、足の指をポリポリもみもみ6してやって、おでこにキスをしてプーさんを抱っこさせ、そのままパパは原稿に向かうというスタイルが確立されました。太陽のように美しい娘。しばらく余裕ができるので一緒に寝てやろうと思います。
なお、家族会議の結果「バンドマンの彼女が歌詞にされるみたいで嫌だ」ということで本のあとがきに「妻の○○○に感謝します」みたいな文言は入れないことが閣議決定されました。代わりにここに記すものであります。ありがとう。
結びに
DroidKaigi 2019にはプロポーザルが採択されてもしなくても日本に一時帰国して参加予定です! 「著者です」Tシャツを着て参加しているので声を掛けてください!
suzuri.jpじゃあの!
Firebase Functionsでデプロイしているバージョンを表示したい
おっす!オラFirebase好き彦!
いまやってる新規サービスはFirebase Functionsを使って開発してて毎日のようにデプロイしてるんだけど、妙な挙動に遭遇したときにそれがどのバージョン*1のFunctionで起こったことなのか特定する必要が出てきた。Functionが実行されるときにそのバージョンがわかればいいな〜と思ったんだけど、環境変数等でパッと取得できそうになかったのでデプロイされているバージョンのコミットハッシュをファイルに書き出して読み込むようにしてみたぞ!もっと良い方法があったら教えてくれよな!
1) コミットハッシュを生成するnpm taskを追加する
一番安直にgitコマンドでコミットハッシュを取ることにした。
<!-- functions/package.json --> "scripts": { "commit_hash": "echo `git rev-parse HEAD` > COMMIT_HASH" }
npm run commit_hash
で動作確認する。 COMMIT_HASH
は echo 'COMMIT_HASH' >> ../.gitignore
とでもしておこう。
2) デプロイ時にcommit_hashを実行する
predeploy
に commit_hash
を追記する。
<!-- firebase.json --> "functions": { "source": "functions", "predeploy": [ "npm --prefix \"$RESOURCE_DIR\" run build", "npm --prefix \"$RESOURCE_DIR\" run commit_hash" ] }
--prefix \"$RESOURCE_DIR\"
がないとプロジェクトルートの package.json
からタスクを探そうとするので注意。
3) Functionから読み込む
あとはFunctionから読み出してログに書くなりレスポンスに含めるなり好きにできる。
import { readFileSync } from 'fs' function readCommitHash(): string { const commitHash: Buffer = readFileSync('COMMIT_HASH') return commitHash.toString('utf8') }
4) deploy
$ firebase deploy
いや〜Firebaseっていいですねっ!
*1:Function自体のバージョンではなく自分のアプリケーションのバージョンね
「ブロリーです…」
若者がドラゴンボールを読んでみたら大しておもんなかった的な記事が非常に話題であります。これに関しては「せやねん」と「せやかて工藤」の両方の感情がございます。
一番申し上げたいことを最初に書くと、結局のところ、不可逆なほどこの世に絶大なるインパクトを与えてしまった作品の凄さというのはその前後を目撃した者にしかなかなか伝わらないものなのであります。
件のエントリの「どこかで見たことのある絵」「よくあるストーリー」という評がいみじくもドラゴンボールの凄さそれ自体を表しているのです。ドラゴンボールが現在に脈々とつづくこの絵柄やストーリーを作ったのです。だから時をさかのぼって現在の作品とドラゴンボールを比べるのは、モーツァルトを聴いて「ありきたりな曲調」という感想を述べるのと同じくらい無意味なことです。浦沢直樹が「浦沢直樹の漫勉」で「大友克洋の衝撃は結局いまのひとが大友克洋の漫画を読んだってわからないんですよ。いまは大友克洋『以後』の世界なんだから」というようなことを言ってましたがまったく同じことです。
ちょっと話は逸れるんですが僕は同じような切なさともどかしさをダウンタウンに感じています。よく若い人が「ダウンタウンなにがおもろいねん」「普通やん」と腐すのを聞いて胸を傷めています。たしかに彼らは歳をとって昔ほどのキレはなくなったかもしれないけど、ダウンタウンの登場は我々の世代にとってドラゴンボールに匹敵する衝撃だった。
バナナの皮に滑って転んでみんなが笑うドリフ的お笑い観を、ゆるいスピードと低いテンション、斜に構えたニヒルでシュールな2000年代のお笑いに変えた。現代お笑いの基礎を作った偉大なコンビなんです。いわば示準化石なんですよ。それ以前と以降で時代がまったく違うんです。いまの普通を彼らが作ったんです。いまだにごっつええ感じのDVDを観ることがあります。いまの人が見ても面白いと思うけど、やっぱりどうしても「この10年前を知ってる人」がこれを見る衝撃とは違うと思うな。
いま求職中でして、よくコーディングクイズを解いています。コンピュータ・サイエンスの世界には「動的計画法」というアルゴリズムがあります。これは、普通に取り組むと解けないような膨大な組み合わせから最適解を得るようなときに効力を発揮するアルゴリズムなんですが、この基本的な考え方は「以前のループの結果を保存しておいて、いまの方が結果が良ければ置き換える」というものです。
漫画も、お笑いも、コンピュータ・サイエンスも過去の偉大な資産があって現在があります。現在は過去の延長線上にしかなく、いまの我々が過去より少し良いものを簡単に手にできるからと言って、それによって過去の栄光にいささかのケチがつくものではありません。先人の達成には常に敬意を払いたいものであります。
ちなみに僕がドラゴンボールで一番好きなシーンはタオパイパイが自分の投げた石柱に乗って移動するシーンですね。物理学的には、投じた石柱に飛び乗る跳躍力があれば目的地までそのままジャンプできることになるそうです。
それじゃあ来週もまた見てくれよなっ。
エンディングはスタンド・バイ・ミーで宜しく頼む
常に冷静沈着、無表情、チート忍術で相棒のケン一氏のテンションを上げたり下げたりする伊賀の忍者ハットリくん。
普段の彼の姿からは想像もつかないが、ハットリくんが涙を流す回があるのをご存知だろうか。
アニメ第187話「忍法虫変化?!の巻」である。
ある日ケン一が学校から帰ると、テレビのクイズ番組に当選したことを知らせるハガキが届く。家族が勝手に応募していたのだ。
優勝賞品はヨーロッパ旅行とあって、お父さんは大張り切り。ケン一にクイズの猛特訓を施すが、人一倍のあがり症とあってケン一は本番前にハットリくんに「緊張をほぐす忍術を授けてくれ」と懇願する。
ハットリくんは「拙者がキリギリスに变化してついてゆくから安心するように」とケン一を諭す。果たしてハットリくんはキリギリスに变化したが、実際にはただのキリギリスを虫かごに入れて渡して自分は隠れ、ケン一を安心させただけなのであった。
特訓の甲斐とハットリくんの加護(と思い込んでいる)あってケン一は順調に正解を重ね、ヨーロッパ旅行にあと一歩まで迫る。
ところがテレビ収録のあまりの照明の熱にキリギリスが瀕死の状態に陥っていることに気付いたケン一は狼狽し、目前にある栄光をすべて放棄して泣きながらキリギリスを介抱することを選ぶ。
傍観していたハットリくんも「ケン一氏、そこまで拙者のことを…」と感涙する。そして、このままキリギリスが死んでは二度とケン一の前に姿を現すことができないと悟ったハットリくんは伊賀の里に伝わる秘伝の薬を使ってキリギリスを蘇生させ、元気になったキリギリスが飛び跳ねた瞬間煙幕を張ってキリギリスと入れ替わる。「いやあ、ご心配をかけたでござる…」
真の友情は伊賀忍者の覆面すらも湿らせるのでござった、ニンニン。
夢
「芸能人やからってかしこまらんでええからね☺️」
部屋に入るとチュートリアルの徳井じゃない方に着席を促された。
「気にせんでええよ、おれもITとか勉強して行きたいなと思てたとこやし☺️」
部屋にはチュートリアルの徳井じゃない方しかいない。
「ほんで、なに?本書いてんの?Androidの、テスト?
うーん、ええんちゃうかなと思うけどね俺は。
ほんで、なに?下世話なハナシやけどどのぐらい売れんの?
千?千かー。うーん。
まあ、アレやん?書くことが大事いうかね。ほら名刺代わりにもなるし」
名刺代わりなぁ。
それからチュートリアルの徳井じゃない方は、芸人の根性を見せるために僕の目の前で本物の中華鍋を使ってアッツアツの八宝菜を炒めはじめ、直接素手で掴んで白菜をムシャムシャ食べ始めた。こっち!めっちゃこっち見てるから!
「まあITやろ?わかるわかる、ロンブー淳にも言うとくし☺️」
という夢を見たよ。
「Androidテスト全書」アーリーアクセス公開と査読のお願い
いま書いている「Androidテスト全書」は初稿がほぼ出揃い、アーリーアクセスが始まりました。僕の担当分では1, 2章をお読みいただけます。
お時間のある方は是非ともフィードバックください。少し時間を置いて読み直してみると、既に自分でも構成のまずい部分や言葉足らずなところが見えてきて忸怩たる思いです。ただ、ここから逃げずに真摯に向き合わないと本は良くなりません。どんな細かいところでも、感じた違和感でも良いので教えていただけると嬉しいです。
それから個人的には3章が僕の担当分の山場でして既に数人の友人に査読をお願いしたのですが、こちらももし僕の知人で興味のある方は是非査読していただけると嬉しいです。
- 非同期処理のテスト
- DBのテスト
- RxJavaのテスト
- 多層アーキテクチャ(MVP)のテスト
- テストのないプロジェクトにテストを導入する
あたりを書いていて識者の意見を強く求めています。公開前原稿をお渡しする関係で、こちらに関しては知り合い限定となりますが、もしTwitter, Facebook, LinkedIn等でつながっている僕の知り合いでご協力いただける方はこそっと教えてください。
新規事業をひとりで作るノウハウ
生存報告も兼ねて。
カリフォルニアに来てもう半年ぐらい経った感覚ですが、実はまだ4ヶ月ほどでした。非常に多くの素敵な方々との出会いがあり、妻も僕も子供もこの皆さまの助けがあってどうにか生きております。どう感謝してよいか言葉にできないほどです。
さて、ビジネス上の僕のミッションは次の3つです。
- 主に投資や連携目的の交渉(の技術面のサポート)
- 日本との連携
- 新規事業の開発
どれもなかなか難しいです。会ってアポぐらいなら応じてくれる会社も多いですが、投資や連携といってもバブル崩壊以後経済成長できていない我が国はもはや「商習慣だけめんどくさいのに今やカネも持ってないから相手にしてられない連中」というのは肌で感じます。ご存知の通り、サンフランシスコ・ベイエリアはIT企業会社員が年収5000万円もらうような場所です。なかなか同じ規模感で会話するのが難しいレベルに達しています。
こみこみという噂のNetflixのPERMデータを見ても、ほんの2年前と比べても、かなり高騰してる気がする。(僕の給料はRSU考えるとむしろ下がっているが。。) pic.twitter.com/9t2nVdIjrn
— Takafumi Yonekura (@tyonekura) August 2, 2018
さて、クヨクヨもしていられないので、僕はプログラマの本分たるプログラミングによって少しでも生きた証を残すより他ありません。渡米以来自分の持ち時間の30-40%ぐらいを使って新規事業のプライベートアルファを公開するに至りました。対象ユーザが弊社内記者ということもあり、現時点でみなさまにお使いいただけないのは残念でしょうがないのですが、たったひとりでもサービスを公開まで持っていけるというのは我々にとって明るいニュースなので書いておこうと思います。
TL; DR いま新規事業やるならFirebase1択
今回自分が作ったのはウェブサービスですが、とにかくFirebaseが強いです。僕がこれから起業するなら、まずはFirebaseで作って運良く流行りそうならお金がかかる部分だけ書き換えるみたいなアプローチを間違いなく採るでしょう。今回使った技術スタックは次のとおりです。
- TypeScript
- Cloud Functions
- Cloud Firestore
- Firebase Authentication
- Google APIs Node.js Client
- Cloud Natural Language
TypeScript
後述するCloud FunctionsがNodeランタイムで動くのでJavaScriptは好むと好まざるとにかかわらず使わざるをえませんでした。僕は強い静的型付けが好きなのと、FunctionsにTypeScriptの例がたまたま載ってたのでTypeScriptを選びました。Flowを選ばなかったのは「店頭に並んでなかった」ぐらいのことで深い理由はありません。
まったく初めてでしたが次の2冊を読んで各30分の計60分でとりあえず書けるようになったのでおすすめです。そもそもC言語の子孫の言語なんてどれも青森弁と山形弁ぐらいの違いしかありません。
比較的快適に開発していますが、関数オーバーロードがさながらC言語のプロトタイプ宣言であるところや、これらで引数に取るインタフェース型がtype erasureによりinstanceofが使えないことから少し持って回ったようなハックが必要な点などいくつかまだ慣れないところはありますが、許容範囲というところです。
tsc watchは文句なく素晴らしく、息をするようにトランスパイルしてくれます。JSをまったく意識することがありません。
ユニットテスト
ユニットテストはchai, mocha, ts-nodeを使っています。このエントリを真似しました。
Unit testing node applications with TypeScript — using mocha and chai
async/awaitを多様しているのでプラスしてchai-as-promisedを入れています。次のように使います。
import * as chai from "chai"; import { expect } from 'chai'; import * as chaiAsPromised from "chai-as-promised"; import 'mocha'; import { NLP } from '../nlp'; before(() => { chai.use(chaiAsPromised); chai.should(); }); describe('NLP', () => { const nlp = new NLP(); it('should return [Amazon Alexa]', async () => { const keyword = "Amazon Alexa の取材"; const list: string[] = await nlp.analyzeEntities(keyword, defaultFilter); expect(list).is.not.empty; }); });
ts-nodeでテストケースを個別に実行する方法がよくわからんかったので
{ "name": "functions", "scripts": { "testall": "mocha -r ts-node/register src/**/*.spec.ts", "test": "mocha -r ts-node/register src/spec/$spec.spec.ts" } }
みたいにして全体実行はnpm run testall, 個別実行はspec=[クラス名] npm run testみたいにしています。
もっといい方法があったら是非コメントください。
namespaceがイマイチ使いこなせない
なにぶん全てひとりでやっており誰もコードレビューしてくれないので経験者からすると眉をひそめるような作法で書いている可能性が高いです。
たとえばnamespaceはKotlinのpackageの感覚でディレクトリを掘ってファイルも分けていますが、次のようにfoo.tsとbar.tsを別ファイルに分けて同じ名前空間の下にぶら下げた場合、
// my_service/foo.ts export namespace MyService { export class Foo {} } // my_service/bar.ts export namespace MyService { export class Bar {} }
利用する側で名前空間がバッティングしてるとimport * as aliasするしかなくてなんか不格好です。これはこういうもんなんでしょうか🤔
たぶんTSの作法がある気がするのでTS Wayをどなたか教えてください。
// import {MyService.Foo} from './my_service/foo' no good!! // import {MyService.Bar} from './my_service/bar' no good!! import * as foo from './my_service/foo' import * as bar from './my_service/bar' export class Baz { qux() { const hoge: foo.MyService.Foo = new foo.MyService.Foo() const fuga: bar.MyService.Bar = new bar.MyService.Bar() } }
Cloud Functions
いわゆるサーバレスにファンクション単位でデプロイできるやつです。これはとにかく素晴らしいです。
export const foo = functions.https.onRequest(async (request, response) => { try { const token: Credentials = await readToken(request.query.account) const resp = doSomething(token) response.send(resp); } catch (error) { sendError(response, error) } });
こんなの書いておくだけで設定したエンドポイントに対応するファンクションが起動します。
HTTPトリガの他にバックエンドで各種イベントトリガに応答して処理を行うことができます。後述の通りPub/Subも使えます。
これの何がいいって、フレームワークだのミドルウェアだのつまらない知識をためなくても一筆書きでとりあえずサービスを提供できるんですよね。
サーバレスというと弊社ではAWS Lambdaを使っていますが僕はCloud Functions 1択でした。それはFirebaseとの連携性です。FirebaseのリアルタイムDBであるFirestoreのドキュメントへのイベントを検知してFunctionをトリガできるのは便利です。他にも認証トリガなんてのもあってアツイです。
Cron Job
単独でcron jobはできませんが、これはGAEと組み合わせればできます。次のエントリに詳しいです。
端的にいうとGAEの持つCron機能をPub/Subで購読するというものです。GAEを一切使ったことがなくてもこのエントリの通りにすれば'hourly-tick'だの'minutely-tick'だのに反応するファンクションを書いておけばトリガされます。
export const batch = functions.pubsub.topic('hourly-tick').onPublish(async (event) => { // do whatever you like });
Local Emulation
ファンクションはデプロイする前にローカルでいくらでも試せます。
Run functions locally | Firebase
リンクにほとんど書いてあるので特に補足はないのですが、firebase serve でローカルにまるっきりfirebase hostingとcloud funcionsのローカル版が立ち上げるのでcurlなりブラウザなりで試すことができます。
firebase functions:shellとかするとTCP/IPではなく対話的シェルのような感じでファンクションごとにエミュレートができます。ここでFirestoreの読み書きもテストできます。
個人的には前者で実際にFirestoreにつなぎに行ってInstrumentedにテストできるので後者はほとんど使ってませんが便利な使い方を今後発見するかもしれません。
それから、OAuth2のリダイレクトとかをテストするときにLocal Emulator環境なのかデプロイ後だったのか区別する必要があり、これはちょっと簡単にわからなかったので環境変数のようなやつを使いました。
Environment configuration | Firebase
firebase functions:config:set my_service.is_local=true
みたいにして設定するとサーバに直接設定されるんですが、ファンクションのディレクトリに.runtimeconfig.jsonという形式で置いておいて適宜中身を書き換えるとローカルエミュレータではその値が使われます。アクセスするのはローカルもデプロイ後も同様です。
firebase functions:config:get > .runtimeconfig.json
const IS_LOCAL: boolean = JSON.parse(functions.config().my_service.is_local); const HOST_NAME = IS_LOCAL ? `http://localhost:5001/${process.env.GCLOUD_PROJECT}/us-central1` : `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`;
Cloud Firestore
これはドキュメントベースのNoSQLなんですが、JSON状のデータ構造のあるパスを購読しておくとそこに対する読み/書き/削除/更新を全部通知してくれます。
オフライン対応してる、というか、利用者はすべてローカルコピーを真として読み書きする作りになっており、「オフラインになった!」という状態が存在しないと考えることができます。
結果、安全にデータの読み書きができます。
オンラインになるとこのローカルコピーは勝手にサーバのマスターデータともいうべきものと同期をかけます。同期後はそれが前述の更新イベントとして飛んでくるだけというわけです。
ここに敢えて貼りませんが、僕は2014-5年からFirebaseユーザでその頃はFirebaseといえばこのリアルタイムデータベース(いまのFirestoreはその後継)しかなく、僕はそれの連載記事を持っていたぐらい愛好者でした。
で、新規事業にあえてこのリアルタイムDBを使う必要もないんですが、別に使わない理由もなくて、割と雑になんでもデータを突っ込んでいます。ここも高くなったら置き換えるぐらいの気持ちです。早すぎる最適化は不要です。
Firebase Authentication
これは認証を楽ちんにしてくれるライブラリです。
Get Started with Firebase Authentication on Websites | Firebase
これもほとんどコメントの必要もないほど充実してるんですけど、認証時にFirebase内で一意なユーザ識別子を持って、あとからGoogleだのGitHubだの別の認証プロバイダで認証したときにそれらを全部ひもづけることができるので、複数の認証プロバイダに対応予定なら激つよです。
一点、この方法だとリフレッシュトークンがどうやらもらえない(リフレッシュ自体はできる)っぽくって、リフレッシュトークンがないと裏でCron Jobでトークンを更新しつつ他のAPIを叩くみたいなのができないので、僕は結局後述するGoogleのOAuthライブラリで手認証するのと組み合わせています。
Google APIs Node.js Client
これがとにかく素晴らしい。Google謹製のNodeライブラリ群ですけど、
- TypeScriptで書かれていて型情報がそのまま利用できるものがほとんど
- 非同期処理はほとんど余すところなくPromiseを返すようになっており、こちらが努力ゼロでasync/awaitできる
もう説明の必要もないですね。async/awaitできると同期的に待ち合わせるように非同期のコードが書けるし、他言語で同様の仕組みをもったプログラマが参入しやすいです。プログラムのバグと苦しみは非同期処理に根ざすものが非常に多いですからね。
一点、Listをmapしながら全てのasyncの完了を待ち合わせるような処理は書き方をすぐ忘れるのでメモしときます。
const tokens: [string, Credentials][] = await readTokens() const resultAll = await Promise.all( tokens.map(async token => { const identifier = token[0] const credential = token[1] return result = await doSomethingWithToken(credential) }) )
あとは複数の非同期処理を一気にdispatchしてzipper関数で待ち合わせてまとめるzipみたいなのはちょっとまだ見つけてません。
まあそのものズバリのRxJSってのもあるしなんとでもなりそうです。
まとめ
「ガチガチにロックインされてそうだけど大丈夫?」的な質問を飲み屋の会話(僕は一滴も飲めませんが)レベルでされたことがあったんですが、それが何か?という感じです。
言語、フレームワーク、思想とプログラマは色んなものにロックインされています。価値検証段階でそれらが何かにべったり依存していることは取るに足らないことだと思います。そもそも僕の稼働時間は僕一人が活動時間の3-4割で実質ひと月ぐらいで書いたので、本当にイケるとなったらいくらでも書き換えたらいいです。
最後にこの開発を開始して僕の人件費を仮にゼロと考えた場合の総コストは…!
ジャーン!開発期間中0ドル、サービスインして本日までの合計は77セントでした!ありがとうございます!