もくテク「弥生で未経験の分野に挑戦したエンジニアたち」を開催しました!

f:id:yayoi_ntake:20211102091952j:plain

こんにちは。たけです。2021年10月14日(木)にもくテク「弥生で未経験の分野に挑戦したエンジニアたち」をオンラインで開催しました。 今月も開催報告をしたいと思います!

イベントページはこちらです。(※開催は終了) mokuteku.connpass.com

今回のテーマ

弥生で未経験の分野に挑戦したエンジニアたち

エンジニアとしてのキャリアは多岐にわたっており、弥生でも様々な選択肢が用意されています。
しかし、キャリアチェンジには不安がつきもので、興味はあってもなかなか踏み切れないという方も多いのではないでしょうか。
そこで今回のもくテクでは、弥生で実際にキャリアチェンジをして新たな分野に挑戦したメンバーに自身の経験を語ってもらいました!

セッションの内容

今回は弥生の開発本部でAIの開発やインフラに携わるメンバーが、それぞれのキャリアチェンジストーリーを発表しました。

1.未経験から始めてハマったAIの世界 / 牛尾

新サービスのために自動仕訳の正解率を上げるというミッションを託され、AI開発に携わるようになった牛尾さん。
未経験の状態からプロジェクトをどのように進めていったか、AIをどのように独学していったかについてお話ししました。
AI未経験の方々に向けてのQ & Aもあり、最後は「興味があれば、是非AIにチャレンジしてみてください」とのメッセージで締めくくりました。

牛尾さんのWantedlyの記事もどうぞご覧ください。 www.wantedly.com

2.プログラミングテスト0点!?それならインフラだ! / 峯岸

学生時代にプログラミングテストで0点を取り、インフラエンジニアを志望していたものの、就職先で任されたのは開発の仕事だった峯岸さん。
めげずにインフラの仕事ができる環境を探し続けた結果、知り合いのいる弥生でインフラエンジニアとしてキャリアをスタートすることができました。
自宅ラボ(※業務用のサーバーやネットワーク機器を活用する自宅の環境)の写真も公開し、インフラ愛をアピールしました。

峯岸さんのWantedlyの記事もどうぞご覧ください。 www.wantedly.com

3.チャレンジするきっかけは何処にあるかわからない / 辻野

新卒で弥生に入社し、開発系チームへの所属を経て、インフラエンジニアへとキャリアチェンジした辻野さん。
技術カンファレンスへの参加をきっかけにバックエンド系の技術に興味を持ち、キャリアパス面談を通じてインフラ系チームに入ることとなりました。
キャリアチェンジに不安を抱えながらも、先輩方に相談しながら自分の道を決めていったことをお話ししました。

4.ベンダーから事業会社の情シスへ。何が変わった、変わらなかった!? / 石田

トリを飾るのは、峯岸さんと辻野さんの所属しているチームでリーダーをしている石田さん。
ハードウェアベンダーのサポートエンジニア、バックオフィス職を経験した後、「もう一度コマンドを打ってみたい」という気持ちもあり弥生に転職してインフラエンジニアになりました。
転職後、初めて聞く専門用語に戸惑いながらも、弥生の本社オフィス移転という大仕事をやり遂げました。

次回のご案内

次回の開催は2021年11月18日(木)です。
テーマは「2021年秋のLT大会」!

弥生の開発本部のメンバーが、日々の業務で得た知見についてのLTを行います!
弥生では10以上のチームに分かれて新卒~ベテランが一緒になり、開発を行っています。
今年もそれぞれのチームで新しい技術や施策の導入が進みました。
色とりどりの"実り"を共有しますので、ぜひご参加ください。

mokuteku.connpass.com

採用について

弥生では、幅広いポジションでいっしょに働く仲間を募集しています!
ポジション等の詳細については以下のサイトをご覧ください。

herp.careers

GooglePlayストアの評価を改善しよう!

弥生モバイルチームのtijinsです。

今日はGooglePlayストアでの評価を改善する取り組みについて紹介します。

まず結果からですが、今回の取り組みによりMisocaアプリの評価が、半年間で3.2→3.7と改善しています。

以前の評価

この取り組みを開始する前、Misocaアプリのレビューには、ネガティブなものが多く並んでいました。

使えないのでアンインストールしました。☆1

ログインできません。☆1

などが多かったです。

ネガティブなレビューが多くなる理由の考察

低評価のレビューを確認すると、トラブルに遭遇したユーザーが問い合わせ窓口としてレビューを利用し、その際に低評価を付けている事が分かりました。

バグや障害があればバグの解消が評価改善の正攻法ですが、稼働率やリテンション率を見る限り、すぐに直せる箇所は無く、もう少し高めの評価になってもよさそうでした。

高評価のレビューを増やすには

レビューへの導線を作る

気に入ったらレビューをお願いしますのような、レビューを促すダイアログを導入しているアプリをよく見かけます。 この方法だと親切なユーザーだけがレビューしてくれるので、高評価が集まりやすくなります。

レビューを促すタイミング

高評価のレビューをしてもらう為には、レビューをお願いするタイミングも重要です。
例えば、クラッシュやエラーの発生に合わせてレビューを促すと、低評価のレビューが集まってしまいます。

Misocaアプリでは、請求書のステータスを入金済みにしたタイミングでレビューを促すダイアログを表示する事にしました。 入金されたタイミングであれば、感情的にも嬉しく、またログインできない請求書を作成できないという事もないはずです。

アプリ内レビュー (In-app review)

ユーザーにレビューを促すダイアログを表示しても
ダイアログを見る → レビューするをクリック → GooglePlayに遷移 → レビューする

と、かなりの手順が必要なので、本当に親切なユーザー以外はレビューしてくれません。

Misocaアプリでは、より効果的にレビューを収集する為、GooglePlayコアライブラリのIn-App reviw API (アプリ内レビュー)を使用する事にしました。

アプリ内レビューを使用すると、レビュー投稿フォームをアプリ内に設置でき、GooglePlayに遷移せず投稿可能です。

アプリ内レビューの動作イメージ
アプリ内レビューの動作イメージ Google Developersから引用

In-App review APIの導入

アプリ内レビュー単独のライブラリはなく、GooglePlay Core SDKを導入します

app/build.gradle

dependencies {
    implementation 'com.google.android.play:core:1.10.2'
    // Kotlinの場合 以下も導入する
    implementation 'com.google.android.play:core-ktx:1.8.1'
}

レビューダイアログの表示

レビュー投稿フォーム(レビューダイアログ)はライブラリから表示され、GooglePlayとの通信もライブラリが行うので、アプリとしてはAPIを呼び出すだけです。
ただし、設計のガイドラインには、以下の注意点があげられています。

  • レビューダイアログを改変しないこと(意図的にしない限り改変できません)
  • レビューダイアログの上にレイヤーを重ねて表示しないこと(クリックジャッキングしないこと)
  • レビューダイアログの表示後、コードから制御しないこと(ユーザーの操作で投稿、またはキャンセルされるのを待つこと)

以下のコードを実行する事でレビューダイアログが表示されます。(表示されない場合もある)

fun requestReview(context: Context){
    val reviewManager = ReviewManagerFactory.create(context)
    reviewManager.requestReviewFlow().addOnSuccessListener { reviewInfo ->
        reviewManager.launchReviewFlow(context, reviewInfo). addOnCompleteListener {
            // APIへのリクエスト完了
            //  投稿 or キャンセル を検出する事はできない。レビューダイアログが表示されない場合もある
        }
    }

}

アプリ内レビュー利用時の注意点

ReviewManager.launchReviewFlow()を実行しても、レビューダイアログが表示されない場合があります。
また、ユーザーがレビューを投稿したのかキャンセルしたのかも取得できない為、未レビューのユーザーに絞ってレビューを促すという制御もできません。
とりあえずレビューが完了しているかを気にせずAPIを実行しても、未レビューのユーザーに限定してダイアログを表示してくれるようです。

もっと改善する

☆4や☆5のレビューが増えても、☆1のレビューが有ると平均はあまり上がりません。
しかし、偶発的なトラブルでも☆1は付いてしまう為、☆1を無くす事は不可能です。

☆1のレビューは全くダメ、使えない!のような短文が多いのですが、使い方が分からなくて困っています。教えてください。のような問い合わせも含まれています。

Misocaには電話・メール・チャットを利用できる問い合わせ窓口があり、GooglePlayのレビューはユーザーからのご意見・ご感想を頂戴する所という位置づけであった為、レビュー上で問い合わせがあっても回答していませんでした。

レビューでも双方向のサポートを提供しトラブルを解消できれば、☆1が付いた後でも評価を改めてもらえる余地がありそうです。

終わりに

ストアの評価が4.0未満だと、検索から入ってきたユーザーはあまりインストールしてくれないようです。
実際、MisocaアプリはiOS版もAndroid版もストアの訪問者数は同じくらいなのに、評価の低いAndroid版のインストール率が低くなっています。
ユーザーを逃してしまうのはもったいないですね。

モバイルチームの求人

弥生のモバイルチームでは、Android、iOSのエンジニアを募集中です!!

www.wantedly.com

www.wantedly.com

もくテクの紹介

弥生では毎月技術イベント「もくテク」を開催しています。11月も開催するので、ぜひご参加ください!

  • 日時:11/18(木) 19:00 〜 20:30
  • 内容:今回のもくテクでは、日々の業務で得られた知見についてのLTを行います。
  • 形式:Zoomミーティング
  • 詳細・参加方法:以下のリンクから参加登録をお願いします! mokuteku.connpass.com

【全体QL会】テストマトリックスはじめました

はじめに

 この記事は、12月のアドベントカレンダーに向けて1件くらい寄稿してほしいと言われて書きました。提出したら、早く仕上がったので開発者ブログに使うと言われました。12月が近付いてきたら「あれ?アドベントカレンダーの記事お願いしましたよね?」って言われるのだと思います。こういう交渉術をフット・イン・ザ・ドア・テクニックと言います。悪用していきましょう。

 私は現在『弥生会計オンライン』『やよいの青色申告/白色申告オンライン』チームのQL(QualityLeader)を務めさせていただいています。私はプログラマー出身なので、品質保証については日々勉強している状況です。よりよい手法を取り入れられるところから取り入れて、仕事を楽にしていっています。
 今回は、会計OL/青白OLのバージョン2.8.1(令和2年分確定申告対応)で取り入れた、テストマトリックスについて紹介いたします。

テストマトリックスとは

探索的テストの弱点を補う探索的テストで、以下のようなマトリックスを作成することにより実施します。 f:id:shimad:20211012152440p:plain
縦軸がテスト対象(機能)、横軸がテスト観点です。
凡例は自由に決めて構いません。画像の例では、「○:バグは発見されなかった」「×:障害報告済」「-:テスト対象なし」となっていますが、最近では次の探索箇所を決定する材料として「件数」を記載するようにしています。

導入した経緯

 担当プロジェクトで終盤に実施した統合テストにおいて、複数のバグが検出されました。品質に懸念が生じ、ピンポイントなバグ修正だけでなく、網羅的な再テストを行う必要に迫られました。もちろん、リリース日は決まっている中で。

 それまでプロジェクトでは、該当機能の結合テストをもう一巡やり直す(リグレッションテスト)という対策が一般的でした。ところが、旧バージョン2.7.1(令和元年分確定申告対応)で行ったリグレッションテストは、工数がかかり過ぎる上に効果が上がらなかったという実績が残っていました。そもそも、結合テストをすり抜けて統合テストの網にかかったバグなので、もう一巡やり直してどの程度効果があるのかという疑問がありました。

 結合テストをすべてメンテナンスし、もう一巡やり直すのが最も正式なのですが、前述の通り既にプロジェクト終盤のためその時間はありませんでした。そこで、短時間で網羅的にテストできる手法として、テストマトリックスを使った探索的テストを導入しました。

手順

  1. 縦軸(テスト対象)と横軸(テスト観点)を完成させる
  2. 横軸の手順と期待結果を明確にする
    例: [保存]
    目的  :​(バグの識別子)の類似バグ確認
    手順  :入力項目を可能な限り入力し、[保存]ボタンを押す
    期待結果:入力したとおりに保存されていること
  3. テストを実施し、凡例に従って結果を書き込む
  4. ×のマスに着目し、次の横軸を追加する
    縦軸で×が多ければ類似障害を、横軸で×が多ければその機能を疑う
    新たな観点が見つかった場合、単純に横軸に追加
  5. 類似障害混入の疑い、新たな観点が無くなるまで、(2)~(4)を繰り返す

結果

 2.8.1(令和2年分確定申告対応)での実施結果としては、23件のバグ報告が挙がりました。内訳としては、19件がソフトウェア障害、2件が設計書誤り、2件がテスト結果判定ミスでした。

 テスト専門のエンジニアは統合テストにかかりきりだったので、製造工程を終えたエンジニア10名に実施していただきました。前述のとおり期限が迫っている中で、人海戦術での実施です。おそらくテストマトリックスなしでは、誰がどこまでやったのか混乱したと思いますが、スムーズに完了することができました。

 該当機能の結合テストをもう一巡実施するとしたらざっと700~800時間程度かかる見積もりでしたが、150時間程度で必要なテストを終えることができました。テスト準備は、テスト観点とテスト対象を洗い出して手順を明確にするだけですので、無視していいくらいのオーバーヘッドです。早いだけでなく、十分にバグを検出することができ、安心してリリースできました。

まとめ

 テストマトリックスについて紹介いたしました。
 担当プロジェクトでは、終盤においてバグを潰し切るために使っています。まずTDDによるテストを行います。そこで漏れてしまったテストは、成果物変更によってテストスクリプトを修正し、再テストを行います。その上で、スクリプト作成からやり直すのはコストに見合わない、類似バグを網羅的に探したい、そんなときにテストマトリックスを使うようにしています。

 感想としては、とても便利です。
 探索的テストはエビデンスが残らず、第三者にも説明しづらいですが、テストマトリックスはそれを解決してくれます。導入コストも学習コストも限りなくゼロです。良い技を覚えたなぁと思う一方、便利だからとなんでもかんでもスクリプトを省略しないように肝に銘じているところです。

 なお、テストマトリックスばかりを褒めてしまいましたが、テストエンジニアが十分な知識をお持ちの場合、手順を指定しない探索的テストも有効な場合があります。誰も見つけられないバグをなぜか見つける人(いわゆるゴッドハンド系)もいらっしゃるので、そういう方には別途、手順を指定しない探索的テストをお願いするようにしています。

従来の探索的テストとの比較

比較内容 テストマトリックスを用いない
探索的テスト
テストマトリックスを用いた
探索的テスト
属人性 実施者によって検出率が大幅に変わる
ただのモンキーテストになる場合がある
実施者に依存しない
観点や再現手順を明確にしているため
網羅性 基本的に保証できない。誰がどこまで実施したのか不明 実施漏れが無い
テストマトリックスがチェックリストの役割を果たすため
再現性 偶然により検出した場合、再現できない場合がある 保証されている
再現手順を明確にしているため
偶然性 高い
観点から漏れているバグを見付けることがある
低い
手順を指定しているため、横道に何かありそうでも打ち切ってしまう

参考にしたテスト手法

探索的テストを対象とする機械学習(SOM) を利用した進行中プロジェクトにおける探索箇所推測手法 「FaRSeT-# / ファルセットシャープ」の提案
https://www.juse.jp/sqip/library/shousai/?id=481
SONAR Testing 効率と客観性を両立した新たなテスト手法
https://www.juse.jp/sqip/library/shousai/?id=450
「FaRSeT-#」「SONAR Testing」は専用のツールも使用した、より高度なテスト手法なのですが、私のチームではすぐに取り入れられる点を参考にさせていただきました。

もくテク「誰でもできる!オンライン勉強会のはじめかたLT」を開催しました!

f:id:tomoe_aizawa:20210921153110j:plain

こんにちは、相澤です。
毎月恒例になってきましたもくテク開催報告です!
2021年09月16日(木)にもくテク「誰でもできる!オンライン勉強会のはじめかたLT」を開催しました。

今回のテーマ

誰でもできる!オンライン勉強会のはじめかたLT

もくテクが、運営チームを一新しオンラインで帰ってきたのが、2021年5月。
運営チームは「縁の下の力持ち」「簀の子の下の舞」として、もくテクを盛り上げてきました。
オンライン勉強会に関わることで、運営メンバーにもたくさんの学びがありました。
工夫を運営メンバー内で共有することでもくテクがさらに良いイベントになるのではないだろうか?
苦労や学びを共有することで、これから勉強会を作る人に役立てられるのではないか?
そんな思いから、運営のメンバーにスポットライトを当てたLT大会を開催しました。

セッションの内容

勉強会の立ち上げ~継続についてを、経験や参入時期の違うメンバーたちが発表しました。

f:id:tomoe_aizawa:20210921151949p:plain

1.勉強会がない会社で勉強会を始める方法 / 開発本部 竹山

オフラインの時期にもくテクに参加して刺激を受けて、以前勤められていた会社で勉強会を立ち上げたメンバーからの発表です。
「勉強会はすぐに始められる」というメッセージとともに、どのような流れで動いたのか、具体例と実際の様子を交えてお話しました。

登壇者が登場する他の記事はこちら note.yayoi-kk.co.jp

2.はじめるときにやったこと / 開発本部 石下

オンラインでのもくテクを立ち上げたメンバーからの発表です。
こちらはエンジニア目線での発表になります。
立ち上げ時はとにかくやることがたくさんあるため、運営に割く時間をきちんと取ることと、継続するためにもやらないことを決める、というポイントを押さえつつ、立ち上げから、企画、イベント本番までの準備までをたっぷりとお話しました。

3.勉強会を起点に採用を加速させる話 / 管理本部 東岡

オンラインでのもくテクを立ち上げたメンバーからの発表です。
こちらは人事目線での発表になります。
登壇者自身も「もくテク」参加をきっかけに転職を決めており、こういった技術イベントがどんな影響をもたらすのかをお話しました。

4.視聴者から運営になってみた / 開発本部 相澤

オンラインのもくテク参加をきっかけに、運営に入ったメンバーからの発表です。
運営に入ったきっかけや、外から見ていて思っていたことと実際に運営をしてみて思ったことについてお話しました。

5.登壇者からもくテクにジョインして感じた理想の運営 / 開発本部 飯田

オンラインのもくテクに登壇後、もくテク運営を行ったメンバーからの発表です。
オンラインとオフラインの登壇を比較したメリット・デメリットを元に、登壇者の目線から運営がどんなことを意識して登壇をフォローしてくれると嬉しいのか、という内容をお話しました。

登壇者が登場する他の記事はこちら tech.yayoi-kk.co.jp

f:id:tomoe_aizawa:20210921153429p:plain

次回のご案内

次回の開催は2021年10月14日(木)です。
テーマは「弥生で未経験の分野に挑戦したエンジニアたち」

エンジニアのキャリアは多岐に渡っており、弥生でもメンバーの特性に合わせて様々な選択肢を用意しています。 しかし、キャリアチェンジには不安がつきもので、興味はあってもなかなか踏み切れないという方も多いのではないでしょうか。
そこで次回のもくテクでは、弥生で実際にキャリアチェンジをして新たな分野に挑戦したメンバーに自身の経験を語ってもらいます。 当時の選択をどう思っていて、今はどんな気持ちで仕事に向かっているのか。それぞれのストーリーをお楽しみください!

ご自身のキャリアについて考えたいあなたのご参加をお待ちしております! mokuteku.connpass.com

おわりに

弥生ではエンジニアを絶賛募集中です! 募集職種はこちら herp.careers

AWS CodeBuildでDockerイメージをビルドする際にビルドフェーズを意識してbuidspecを書く

システム開発部Misocaチームエンジニアの id:mizukmb です。

Misocaチームでは、DockerイメージのビルドをAWS CodeBuildを利用して日々ビルドをしています。

CodeBuildではbuildspecと呼ばれるファイルに docker builddocker push といったコマンドを記述することで、CodeBuildを起動した際にこれらのコマンドを自動で実行することができます。

このbuildspecにはビルドフェーズがあり、それぞれのフェーズに適した処理を記述する事が推奨されています。この辺りを意識してDockerイメージのビルドからECRのプッシュまでをbuildspecに記述したいと思います。

ビルドフェーズとは

CodeBuildには phases/installphases/build といった決められたビルドフェーズがいくつかあり、各フェーズに適したコマンドを配置する事がAWSのドキュメントにより推奨されています。

docs.aws.amazon.com

例えば phases/install はAWSのドキュメントでは以下のように書かれています。

install フェーズは、ビルド環境でのパッケージのインストールにのみ使用することをお勧めします。たとえば、このフェーズを使用して、Mocha や RSpec などのコードテストフレームワークをインストールすることができます。

( https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html より引用 )

また、ビルドフェーズは各フェーズで成功/失敗した場合にどのフェーズに遷移するかが決められています。このフローチャートは以下のドキュメントの ビルドフェーズの移行 から確認できます。

docs.aws.amazon.com

これらを意識して書かれたbuildspecについて次の章にて解説します。

buildspec

app:latest というタグ名のDockerイメージをビルドし、ECRにプッシュする。プッシュの前にDockerイメージの内容が期待通りであるか Gossを使ってサーバーテストを実行し、失敗したらプッシュを行わずにCodeBuildの実行を終了する。

というような処理を先ほど説明したビルドフェーズを意識して書いてみると以下のような内容になります。

version: 0.2
phases:
  install:
    commands:
      - curl -L https://raw.githubusercontent.com/aelsabbahy/goss/master/extras/dgoss/dgoss -o /usr/local/bin/dgoss
      - chmod +rx /usr/local/bin/dgoss
      - curl -L https://github.com/aelsabbahy/goss/releases/download/v0.3.16/goss-linux-amd64 -o /usr/local/bin/goss
      - export GOSS_PATH=/usr/local/bin/goss
      - export GOSS_FILES_PATH=config/docker/spec
  pre_build:
    commands:
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com
  build:
    on-failure: ABORT
    commands:
      - docker build -t app:latest .
  post_build:
    commands:
      - dgoss run app:latest
      - docker tag app:latest  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:latest
      - docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:latest

それぞれのフェーズについて先ほど紹介したAWSのドキュメント CodeBuild のビルド仕様に関するリファレンス - AWS CodeBuild の内容をベースに解説します。

phases/install

install フェーズではビルド等に必要なパッケージのインストールが推奨されています。 aws , docker コマンドはCodeBuild実行環境にはプリインストールされているので、このフェーズでは goss とDockerイメージをテストするためのラッパースクリプト dgoss をインストールします。環境変数 GOSS_PATHGOSS_FILES_PATHdgoss の実行に必要なので一緒に設定します。

gossdgoss のインストールについてはこちらのREADMEを参考にしています

  install:
    commands:
      - curl -L https://raw.githubusercontent.com/aelsabbahy/goss/master/extras/dgoss/dgoss -o /usr/local/bin/dgoss
      - chmod +rx /usr/local/bin/dgoss
      - curl -L https://github.com/aelsabbahy/goss/releases/download/v0.3.16/goss-linux-amd64 -o /usr/local/bin/goss
      - export GOSS_PATH=/usr/local/bin/goss
      - export GOSS_FILES_PATH=config/docker/spec

phases/pre_build

pre_build フェーズではビルド前に必要なコマンドを実行する事が推奨されています。ECRにサインインしておきます。

  pre_build:
    commands:
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com

phases/build

build フェーズではビルドを実行するコマンドを記述する事が推奨されています。今回のメインである docker build を記述します。

docker build が失敗してDockerイメージが作成できなかった場合、 post_build フェーズに移行してもサーバーテストの実行やECRへのプッシュを行えないので on-failure: ABORT を設定してビルドを中止します。

  build:
    on-failure: ABORT
    commands:
      - docker build -t app:latest .

phases/post_build

post_build フェーズはビルド後に行うコマンドを記述する事が推奨されています。ここではGossの実行やECRへのDockerイメージプッシュを行います。

  post_build:
    commands:
      - dgoss run app:latest
      - docker tag app:latest  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:latest
      - docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:latest

おわりに

ビルドフェーズを意識したDockerイメージのビルドとプッシュを行うbuildspecの書き方について書きました。CodeBuildはビルドフェーズを意識せずに例えば全てのコマンドを install フェーズに書いても期待通りに動作します (今回でいえばECRのプッシュまで出来てしまう)。ですが、ビルドフェーズを意識することでより可読性の高く、正しく失敗するbuildspecを書く事ができます。

採用

弥生ではエンジニアを募集しています。

herp.careers

参考文献

Androidアプリのテストを自動化

こんにちはmisocaアプリの評価が3.5になって嬉しいtijinsです。

今日はAndroidアプリのインストルメンテーションテストについて紹介します。

Misocaアプリのテスト

MisocaアプリはCIによる自動テストとリリース前の手動テストを行っているのですが、開発スピードと品質のバランスを保つ為、手動のテストの自動化を進めています。

3種類の自動テスト

Misocaアプリでは3種類の自動テストを使用しています。 今回は、インストルメンテーションテストについて説明します。

インストルメンテーション テスト

エミュレーターまたは実機上で実行されるテストです。 実際のアプリを動作させているので手動テストと同等の検証が可能ですが、実行速度が遅いです。

Unitテスト

開発PCのJVM上で実行されるテストです。 エミュレーターを介さない為、高速なテストが可能ですが、AndroidOSやContextに依存する処理はテストできません。 (Robolectricを使用すればAndroidOSに依存する部分もテスト可能ですが、非対応の機能がありインストルメンテーションテストの完全な代替には至っていません)

スクリーンショット比較テスト

レイアウトやテーマの異常を見つけるテストです。 インストルメンテーションテスト中にSpoonを使用して保存したスクリーンショット画像で、レイアウトの崩れなどをチェックしています。

サンプルアプリ

今回のテストコードが対象にしているのは、以下のサンプル用アプリです。

サンプルコード

  • メイン画面で編集ボタンを押すと、編集画面に遷移する
  • 編集画面で保存ボタンを押すと、編集された文字列がメイン画面に反映される
  • メイン画面で共有ボタンを押すと、IntentChooser付きで共有される

インストルメンテーションテスト

テスト対象アプリの操作にespressoを使用します。

espresso-intentsは、Activity間の遷移を検証したり、ファイル選択等外部のアプリと連携する部分のスタブ化に使用しています。

Espressoを使ったテスト

以下は、メイン画面から編集画面に遷移し、編集結果がメイン画面に反映される事を確認するサンプルです。

app/build.gradle

dependencies {
    // InstrumentTestで使用するライブラリ
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

SampleActivityTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleActivityTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun testEdit() {
        // メイン画面で実行される
        Espresso.onView(ViewMatchers.withId(R.id.btn_edit)).perform(
            ViewActions.click()
        )

        // 編集画面で実行される
        // 初期表示の確認
        Espresso.onView(ViewMatchers.withId(R.id.edit_text)).check(
            ViewAssertions.matches(
                ViewMatchers.withText("Hello")
            )
        )
        // EditTextに入力する
        Espresso.onView(ViewMatchers.withId(R.id.edit_text)).perform(
            ViewActions.replaceText("Bye!")
        )
        // 保存ボタンをクリックする
        Espresso.onView(ViewMatchers.withId(R.id.btn_save)).perform(
            ViewActions.click()
        )

        // メイン画面で実行される
        // 編集結果が反映されている
        Espresso.onView(ViewMatchers.withId(R.id.txt_text)).check(
            ViewAssertions.matches(
                ViewMatchers.withText("Bye!")
            )
        )
    }
}

Espresso-intentsを使ったテスト

startActivity()で送信されたIntentの検証や、ファイル選択など他のアプリと連携する部分のスタブ化が可能です。

送信されたIntentの検証(IntentChooserが無い場合)

app/build.gradle

dependencies {
    // InstrumentTestで使用するライブラリ
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    // import androidx.test.ext.truth.content.IntentSubject.assertThatに必要です
    androidTestImplementation 'androidx.test.ext:truth:1.3.0'

    // espresso-intents
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'

    // 戻るボタンの操作などに利用します
    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}

SampleIntentsTest.kt

@RunWith(AndroidJUnit4::class)
class ExampleIntentsTest {

    // IntentTestRuleを使用します
    @get:Rule
    val activityRule = IntentsTestRule(MainActivity::class.java)

    // バックキーを操作する場合に必要です
    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    @Test
    fun testEdit() {
        Espresso.onView(ViewMatchers.withId(R.id.btn_edit)).perform(
            ViewActions.click()
        )

        // getIntents()はactivityが起動されてから発行されたIntentのリストを返します
        val intents = Intents.getIntents()
        val intent = intents.first()
        assertThat(intent).hasComponent(
            ComponentName(
                "jp.misoca.sampleintentstest",
                "jp.misoca.sampleintentstest.EditActivity"
            )
        )
        // androidx.test.ext.truth.content.IntentSubject.assertThatを使用します
        assertThat(intent).extras().string(Intent.EXTRA_TEXT).isEqualTo("Hello")
    }
}

送信されたIntentの検証(IntentChooserが有る場合)

IntentChooserでラップされたIntentでは Intent.EXTRA_INTENTに元のIntentが入っています。

    @Test
    fun testShare() {
        Espresso.onView(ViewMatchers.withId(R.id.btn_share)).perform(
            ViewActions.click()
        )

        // IntentChooserが使用されている
        val chooser = Intents.getIntents().first()
        assertThat(chooser).hasAction(Intent.ACTION_CHOOSER)

        // 元のIntentを取得する
        val intent = chooser.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
        assertThat(intent).hasAction(Intent.ACTION_SEND)
        assertThat(intent).hasType("text/plain")
        assertThat(intent).extras().string(Intent.EXTRA_TEXT).isEqualTo("Hello")

        // バックキーを操作してChooserを閉じる(Chooserを閉じるまで次のテストに遷移しない為)
        device.pressBack()
    }

startActivityForResult()をスタブ化する

indending()使用するとstartActivityForResult()の実行後`onActivityResult()`がすぐ実行されます。 画像選択など、他のパッケージに依存するテストに便利です。

    @Test
    fun testOnEdit() {
        // onActivityResultに入力されるダミーデータ
        val resultData = Intent().apply {
            putExtra(Intent.EXTRA_TEXT, "Bye!")
        }
        val result = Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)

        // intendingを使用して、編集画面をスタブ化する
        // androidx.test.espresso.intent.Intents.intendingを使用します
        intending(
            IntentMatchers.hasComponent(
                ComponentName(
                    "jp.misoca.sampleintentstest",
                    "jp.misoca.sampleintentstest.EditActivity"
                )
            )
        ).respondWith(result)

        // intendingされているので、編集画面が起動せずにonActivityResultが実行される
        Espresso.onView(ViewMatchers.withId(R.id.btn_edit)).perform(
            ViewActions.click()
        )

        //結果をチェック
        Espresso.onView(ViewMatchers.withId(R.id.txt_text)).check(
            ViewAssertions.matches(
                ViewMatchers.withText("Bye!")
            )
        )
    }

宣伝

弥生モバイルチームは先月1名メンバーが増えて拡大中です。 AndroidもiOSのエンジニアまだまだ募集中です!! www.wantedly.com

www.wantedly.com

Rails 6.1の新機能:dependent: :destroy_async を使ってみた

はじめに

こんにちは、 id:RKTM です。 先日乗鞍岳にバックカントリースキーに行きました。昔は弱音を吐きながら必死で下りたルートを、今回は気持ちよく颯爽と(主観ですが)滑れるようになり、スキー技術の上達を感じて嬉しかったです。

f:id:RKTM:20210510131117j:plain
位ヶ原から乗鞍岳剣ヶ峰方面

Rails 6.1の新機能:dependent: :destroy_async を使ってみた

さて、最近MisocaはRails 6.1にアップデートしました。*1

早速Rails 6.1の新機能 dependent: :destroy_async を導入しましたので、その紹介をします。

dependent: :destroy_async は、関連を非同期で削除する

Railsの関連を設定する際、 親 has_many :子, dependent: :destroyという設定を書いた人は多いと思います。これは「親のレコードを削除する際、関連する子レコードも合わせて削除する」というものです。 dependent: :destroy_async は同じく子レコードを削除するのですが、削除処理を非同期で行う、というものです。名前の通りでわかりやすいですね。

dependent: :destroy_async はこちらのPull Requestで提案され、マージされました。 github.com

使いどころ:関連先のレコード数が多い場合

dependent: :destroy で指定した子レコードが多い場合、削除に非常に時間がかかることがあります。特に多くの子レコードが多くの孫レコードを持つ場合は要注意です。オンラインで処理すると最悪の場合リクエストがタイムアウトする可能性があります。

dependent: :destroy_async を利用することで、時間のかかる削除処理はキューに積み、クライアントに早期にレスポンスを返すことができます。

Misocaで困っていた点

MisocaではMisoca APIの利用のために doorkeeper-gem を使っています。doorkeeper-gemのお作法に従い、下記のようなテーブル関連となっています。(AccessGrantも存在していますが、複雑になるため割愛しています)

f:id:RKTM:20210511160822p:plain

Userが親レコードであり、OauthApplicationが子レコード、AccessTokenは孫レコードとなります(これらは has_many :xxx, dependent: :destroy で関連を定義していました)。

User削除時(≒退会)に、これらレコードの削除に非常に時間がかかる事例が問題となりました。

その原因はMisocaではAccessTokenが非常に多く生成されていたためでした(実際には、indexが張られていなかった、などの理由もありましたが…)。

ということで、Userの削除時にOauthApplicationから先は非同期で削除することになりました。

改修

改修は簡単で、下記のように書き換えるだけです。

before

# User model
  has_many :oauth_applications, dependent: :destroy

after

# User model
  has_many :oauth_applications, dependent: :destroy_async

動作確認

自動化されたテストも当然書きますが、ここでは動かして確認してみましょう。

  • User.last: 削除対象のユーザー。
  • User.first:上記とは別のユーザー。 削除対象ユーザーのOauthApplicationに紐づくAccessTokenを保持する。

とします。

ジョブを停めておく

非同期で走るかどうかを確認するため、ジョブを停めておきます。 以降、DelayedJobを使っていますが、ご利用の非同期ライブラリに応じてコマンドを実行してください。

削除対象のユーザーにOauthApplicationを作る

rails console

# 2つ登録しておく
 User.last.oauth_applications.create(name: "aaa", redirect_uri: "http://localhost:3000/")
 User.last.oauth_applications.create(name: "bbb", redirect_uri: "http://localhost:3000/")

上記で追加したOauthApplicationに対し、別のユーザでAccessTokenを作る

User.first.oauth_access_tokens.create(application_id: OauthApplication.last.id)

User.first.oauth_access_tokens # 作られたレコードが表示される

削除対象のユーザーを削除する

User.last.destroy

そうするとジョブが登録されます。

  TRANSACTION (0.4ms)  BEGIN
  ↳ (snip)
  Delayed::Backend::ActiveRecord::Job Create (0.7ms)  INSERT INTO `delayed_jobs` (snip)
  # 下記のようなJobが登録される
  # job_class: ActiveRecord::DestroyAssociationAsyncJob
  # owner_model_name: User
  # owner_id: 317
  # association_class: Doorkeeper::Application
  # association_ids: # ↓削除対象のユーザーが持つOautApplication2つが対象になっている。
  # - 11 
  # - 12
User.last

ユーザーは削除されています。

ジョブは停めてあるため、OauthApplicationはまだ削除されていません。

OauthApplication.all # 削除対象のユーザーが持つOautApplication2つはまだ削除されていない

同じく、別ユーザのAccessTokenは残っています。

User.first.oauth_access_tokens # 作られたレコードが表示される

ジョブを起動して削除処理を走らせる

それではジョブを起動して非同期削除が行われるか確認してみましょう!

OauthApplication.all # 削除対象のユーザーに紐づくoauth_applications は削除される

User.first.oauth_access_tokens # 削除されたoauth_applicationsに紐づくレコードが削除されている

はい、意図通り動きました。

挙動で気になる箇所を調べる

has_many, dependent: :destroy_async で関連レコードが存在しない場合はどうなるの?ジョブは登録されるの?

ジョブは登録されません。無駄にキューを埋めたりはしない実装となっています。

Offer dependent: :destroy_async for associations by adrianna-chang-shopify · Pull Request #40157 · rails/rails · GitHub

has_many, dependent: :destroy_async で関連レコードが複数存在する場合、ジョブはそのレコード数分登録されるの?そうなるとキューが溢れるのでは?

上記のリンク先の周辺を見るとわかるとおり、関連レコードが複数存在する場合であっても、登録されるジョブは1つです。 association_ids: ids とあるように、1ジョブで複数の子レコードのidを受け取れるようになっています。

導入における注意点

キュー溢れに注意

dependent: :destroy_async をネストするなどで大量にジョブが登録されるとキューが溢れる可能性があります。既存の非同期処理への影響も考慮して設計しましょう。

非同期ジョブが失敗することも考慮する

親を削除した時に "必ず即座に" 削除されてほしい関連(≒子レコードの削除に失敗したならロールバックして親レコードも残って欲しい)には使わないほうが良いでしょう(適切な例は思いつきませんでした)。

最後に

Rails 6.1新機能 dependent: :destroy_async を紹介しました。この機能を導入することでMisocaでは削除処理の懸念事項が減りました。 便利な機能ですので、ぜひ利用を検討してください。

*1:Rails 6.1リリースから期間が空いた理由は、Regression on Optimistic Locking from 6.0 · Issue #40786 · rails/rails のissueに遭遇したため。