【全体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に遭遇したため。

Misocaフロントエンドの歴史と未来

こんにちは、 @mugi_uno です。

Misocaがサービスローンチされたのは 2011年です。実は2021年は10年目ということで何気に節目の年だったりします。

10年もあれば世の中的にもさまざまな技術変遷があり、Misocaもその波に乗っていけるよう、日々改善を繰り返してきました。

というわけで今回は、私自身がフロントエンド側の作業を多くやってきたこともあり「この10年間でMisocaのアーキテクチャがどのように変わってきたのか?」をフロントエンド側に焦点を絞って振り返ってみたいと思います。

※ 意思決定に関する資料が無いものも存在するため、一部は情報に基づく推察になる点をご承知おきください。

年表

いきなりですが、ざっくり年表を書いてみました。

f:id:mugi1:20210409153731p:plain

上部の黄色いラインは、フロントエンドに大きい影響を与えたMisocaの機能です。

これをもとに、サービスローンチから順を追って見てみます。

創成期 / 2011 サービスローンチ〜2015

いきなり4年経過していますが、この頃はある程度固定化されたアーキテクチャで開発が進められていました。

f:id:mugi1:20210409155505p:plain

ベースとなる技術要素は

  • jQuery
  • CoffeeScript
  • Bootstrap
  • Sprockets (Asset Pipeline)

と、当時Railsでアプリケーションを組む際に主流だったものが利用されています。

当時はまだ npm によるパッケージ管理なども導入されておらず、jQuery UI のような依存ライブラリは、直接リポジトリ内にダウンロードしたファイルが配備されていました。

また、recalc という魔法の金額計算関数が爆誕し、長きに渡って良い意味でも悪い意味でも大活躍します。

フロントエンド改善のはじまりと案件機能 / 2015-2016

ローンチからしばらくはMisocaフロントエンドの技術スタックに大きい変化はありませんでしたが、この頃から徐々に新しいライブラリが導入されるなど、いくつかの改善が施されるようになっていきます。

f:id:mugi1:20210409155938p:plain

Vue.js 導入

機能が増えていくと同時に、フロントエンド側のコードも徐々に増えていき、jQuery だけですべてを制御することの課題感が出てきたころです。

そこで、検討の結果 Vue.js (当時のバージョンは 0.12) が導入されました。 他にも Angular や Backbone なども候補に挙がっていたようですが、重厚ではなく薄いものがいい、ということで Vue.js が選ばれました。

現在の視点で見ると「Vue.js が薄い?」と思いますが、当時の Vue.js は Single File Component も存在しないため webpack などのビルドも必要なく、かつ Rails が出力するテンプレートをそのまま利用できるなど、コストを低く導入できる点なども採用理由でした。

一気にすべてを書き換えることは厳しいため、このあたりから時間がある時に徐々に Vue.js へ書き換えるようになりました。

パッケージ管理とBrowserifyによるビルド

依存ライブラリはファイルを直接リポジトリに配備して管理していましたが、Vue.js の導入とほぼ同時期に、npm によるパッケージ管理と Browserify によるビルドが組み込まれました。依存が増えて手動による管理に限界が出てきたと思われます。

今でこそ webpack が大活躍していますが、当時は Browserifyや Bower が広く使われていたころです。

Misocaでは browserfy-rails という Gem を介して Browserify ビルドを実行していました。

RubyMineでデバッグしたときのブログが残っていますね。

tech.misoca.jp

案件機能と React & Redux

とある実現しなかった機能開発で React & Redux が試験導入され、そのノウハウをもとに 「案件」という機能が React & Redux を利用した SPA で構築されました。

それまではフロントエンド部分の開発はあっても、基本的には Rails Way に乗った開発スタイルでしたので、技術的には挑戦的なテーマで、 社内的にもモダンフロントエンドのノウハウを蓄積できるいい機会だったのではないかと思われます。

tech.misoca.jp

フロントエンド開発生産性の改善 / 2017-2018

世の中的にもフロントエンドに求められる範囲がどんどん拡大していたころですが、Misocaではアーキテクチャ的に大きい更新が無く、客観的に見るとレガシー化が進んでました。一部のコア機能は Vue.js 化されていましたが、積極的に活用されているとはいえず、新しくコードを書くときは jQuery & CoffeeScript が現役でした。

開発生産性への影響に限らず、採用面などでもデメリットが大きいため、このころから脱レガシー作業に多く取り組むようになります。

f:id:mugi1:20210409161941p:plain

Webpacker 導入

ビルドに利用していた browserify-rails ですが、日常的にフルビルドに数分かかる状態になっており、開発時の大きなフラストレーションになっていました。

そこで、ちょうど同時期に Rails 5.1 で導入された Webpacker を試したところ、15秒-20秒ほどでビルドが終わることがわかりました。

トレンド的にも webpack が完全に主流になっており、流れに乗る意味でも Webpacker が正式導入されました。 JSのビルドは browserify-rails と Sprockets の両方を使っていた状態でしたが、これによって Sprockets はCSSと画像のビルドのみを行う存在になりました。

tech.misoca.jp

Vue.js の最新化

当時の Vue.js の最新バージョンは 2.4 で、いまでこそ当然の Single File Component をはじめとする、多くの魅力的な機能が利用可能でしたが、Misocaでは2015年に Vue.js を導入してから更新されておらず 0.12 のまま止まっていました。

jQuery を使ってコードを書き続けるのは将来を見据えた場合に望ましくなく、かといって Vue 0.12 で新しいコードを書くのももはや負債を増やす事になりかねません。

Webpacker の導入で Vue 2.x 系のビルドを組み込むことが可能になったため、それを利用して Vue.js の最新化が行われました。 これ以降は、 Dependabot などで継続的な更新を続けるようになり、2.x 系の最新版が維持されています。(3.x系への更新は IEの絡みにより未着手です。)

tech.misoca.jp

CoffeeScript → ESNext へ

Vue.js の最新化をする際に、多くのコードの書き換えが必要となりました。

その際に、CoffeeScriptで新しく書くのは気が引けたため、併せてESNextへの書き換えが実施されました。 これ以降はすべてのコードが ESNext で書かれるようになります。

いま思えば、このタイミングで ESNext 化をしていなかったら、後々の TypeScript 導入時に地獄を見ていたのかもしれないな..と思います。

CI周りの整備 (ESLint、テストコード)

CoffeeScript から ESNext に移行したことで、ESLintによるコード分析が可能になりました。 各エンジニアのセンスと好みによっていい感じにレビューで指摘していたものが、ESLintによって検査される秩序ある世界に変わりました。

また、Vue.js 最新化や ESNext への置き換え時は Feature Spec と人力テストにすべてを委ねるドキドキの作業で、精神衛生上とてもよろしくないものでした。

フロントエンドのテストコードが一切存在しないことが改善時の大きな負担になるのを痛感し、フロントエンド単体で Mocha を利用した テストコードを書けるように整備されました。

さらなる改善 / 2019-2020

ある程度全体的な土台が整備されてきたため、+αの挑戦的な取り組みを始めたり、長らく手がつけられなかった秘伝のタレ的なコードにもメスを入れはじめた頃です。

f:id:mugi1:20210412104232p:plain

金額計算処理のリファクタリング

サービスローンチ時からずっと支えてきた「recalc」という魔法のグローバル関数がありましたが、魔法すぎて全体像を誰も把握できておらず、改善しようにも手がつけられない状態が長く続いていました。

そんな中、国で軽減税率制度の導入が決定したことで、魔法とかそんなことは言ってられない状態になりました。

そこで、魔法の関数 recalc は廃止され、計算ロジックだけが npm パッケージとして外部に切り出されました。

それまでの改善の積み重ねによって、テストコードをはじめとするリファクタリングを支える土台は整っていたので、なんとか実現できました。

tech.misoca.jp

Webpacker → webpack

導入コストの低さから browserify-rails に取って代わる形で導入された Webpacker ですが、フロントエンドで必要とする依存の増加に伴い、Webpacker のまま開発を続けるのに支障が出てくるようになりました。

Webpacker の恩恵を受けるフェーズは終了したと判断し、純粋な webpack ビルドへ置き換えられました。

tech.misoca.jp

Mocha→Jest

世の中の潮流に併せて、テストコードが Mocha から Jest に移行されました。

それまでは mocha-webpack を利用しており、実行も重たい状態でしたが、Jest によって大きく改善されました。

また、Jest によって提供されるスナップショットテストなども一部で利用されるようになりました。

TypeScript の導入

2021年現在では完全にメインストリームと化した TypeScript ですが、 Misocaでは同年に実施された金額計算処理のリファクタリングのような作業のリスクを軽減する目的で導入されました。 さすがに学習コストも高かったので、社内ハンズオンなども実施されています。

当初は strict: false の緩い状態での導入で、自動推論で恩恵を受けられる部分だけ受けておこう、というスタイルでした。

tech.misoca.jp

デザイン周りの大幅な改善

この頃は、デザインに関する大きな改善進捗もありました。

まず、デザイナーの皆さんにより「ユーザー体験統一」というプロジェクトが旗揚げされました。

tech.misoca.jp

これにより、Misoca全体でデザインに関する統一した指針 (Misoca Design System) が定まり、プロダクトに適用されました。

さらに、それまで大活躍していた Bootstrap もすべて引き剥がされ FLOCSS に書き直されました。 CSSの詳細度バトルから解放され、秩序が訪れます。

tech.misoca.jp

また、CSSビルドが Asset Pipeline から webpack に乗る形に置き換えられました。 CSS周りでの依存関係もきちんと管理されるようになり、日常的な更新も可能になりました。 同時にSprocketsの役割はさらに小さくなり、現在はごく一部の画像のビルドのためだけに利用しています。

SPAによる機能実装

いくつかの機能がSPAで機能実装され、あわせていくつかの新しい技術要素にトライしました。代表的なものは Vue Composition API と GraphQL の2つです。

それまでは Vue 2.x の Option API で開発をしていましたが、TypeScriptとの相性で厳しさを強く感じていました。 Vue Composition APIは、Vue 3.x 系以降で追加される Vue.js における新しいAPI で、従来とは大きく記法が異なりますが、TypeScript との親和性が高いです。

これ以降は、Misocaでは積極的に Vue Composition API が利用されるようになります。

また、フロントエンド⇔バックエンド間通信に GraphQL が導入されました。実験的な側面も大きかったように思いますが、型定義面などで感じられる恩恵も大きく、現在も新規API作成時は GraphQL を利用しています。

tech.misoca.jp

現在と未来 / 2021-

f:id:mugi1:20210412114713p:plain

現在は次のようなことに取り組んでいます。

Vue Composition API への書き換え

すべてのコードが Vue Composition API であるのを理想として徐々に書き換えています。

Vue 3.x でも Option API は引き続き利用可能ですが、2つの記法が混在していると混乱を招くのと、TypeScriptとの相性を考えて 基本的に Composition API に統一しようとしています。

TypeScriptの型定義の強化

導入時は strict: false でゆるゆるの状態のTypeScriptでしたが、現在は strict: true に切り替わり、将来に委ねて FIXME_any という定義で逃げていた any 利用箇所もすべて厳密な型定義に置き換えました。

他にも型検査的に怪しい箇所が多数残っている状態ですので、日々コツコツと手を加えている状況です。

React & Redux 機能の廃止

2016年に案件機能と同時に導入された React & Redux は、機能自体が提供終了の運びとなり、同時にコード上からも姿を消す予定です。 今後しばらくは Vue.js に一本化して開発していくことになると思います。

【予告】案件機能を終了します | 請求書作成サービス「Misoca(ミソカ)」

今後の課題

少しずつ改善されてはいるMisocaフロントエンドですが、課題は多く残されています。

代表的なものはjQueryで、サービスの中でもコアな部分でjQueryコードがまだまだ現役で動いています。 jQueryであるがゆえに影響範囲が読みづらく、それによる変更時の不具合もちらほら発生しているため、なんとかする必要があります。

また、GraphQL化も道半ばで、新規作成したAPIと既存APIの一部はGraphQLに移行しましたが、完遂しておらず RESTと混在している状態です。 それによってTypeScript型定義がごちゃごちゃしている部分もありますし、今後も継続して対処していく必要があります。

他にも Vue3.x導入を目指した対応・テストコードの拡充など、やれることはたくさんあります。

おわりに & 未来の話

歴史を振り返ってみると、改善の土台の上に新しい改善が乗っており、そこから新しい技術挑戦へと繋がっているように感じます。

フロントエンドは技術変遷が早いと言われ、日々新しい技術要素が登場しています。 それは同時にフロントエンド側に求められる機能要件の範囲が広がっていることを意味し、Misocaの機能開発も例外ではありません。

サービスとして何か新しいことをやりたい・実現したいときに、「レガシーなのでできません」と言うことができるだけないよう、 コツコツした積み重ねで準備しておくのが大事なのかもしれません。

日々精進ですね。自分も頑張りたいと思います👍

MackerelのカスタムメトリックをNew Relicに移行する時はNew Relic Flexという機能がおすすめ

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

Misocaチームでは監視ツールとしてNew Relic Oneの採用を決定し、現在はインフラの監視に使用しているMackerelからの移行作業を進めています。

newrelic.com

Mackerelにはカスタムメトリックという独自のメトリック投稿機能がありますが、こちらをNew Relicに移行する方法について書きたいと思います。

Mackerelカスタムメトリックの代替候補

MackerelカスタムメトリックからNew Relicに移行するには 任意の数値をメトリックとしてNew Relicに送信できる 機能が必要です。私が調査した限りでは以下の機能が代替できそうでした。

  • Custom metrics
    • APMの機能。アプリケーションコード内で専用のメソッドが呼ばれる事でメトリックを送信できる
  • Metrics API
    • 専用のAPIエンドポイントにメトリックとして渡ししたい数値をJSONペイロードに入れてPOSTリクエストすると送信できる
  • New Relic Flex
    • New Relic Infrastructureエージェントに同梱された機能。専用のymlファイルを用意すると定期的にコマンドを実行し、標準出力で得られた値をNew Relicにメトリックとして送信できる

今回はMackerelのカスタムメトリックに機能的に最も近いNew Relic Flexを採用しました。

New Relic Flexの設定・動作確認方法について

New Relic FlexはInfrastructure agentがインストールされたサーバーであればすぐに使う事ができます。

docs.newrelic.com

github.com

/etc/newrelic-infrastructure/integrations.d/ 以下にNew Relic Flex用のymlファイルを置き、agentを再起動する事でメトリックの送信が開始されます。

基本的な使い方はGitHub READMEのbasic tutorial から読み始めると理解しやすいと思います。また、ymlファイルの設定については、 configureexamples が役立ちました。

動作確認には /var/db/newrelic-infra/newrelic-integrations/bin/nri-flex コマンドが便利です。このコマンドはInfrastructure agentがインストールされたサーバーであれば一緒にインストールされます。チュートリアルの4. How to add more Flex integrationsに使い方が記載されていますが、以下のようにymlファイルを指定する事でNew Relicにはメトリックを送信することなく、New Relicにどのような形式でメトリックが送信されるのか、意図した通りのメトリックになっているのかといった事が確認できます。

# myconfig.yml を対象に動作確認する。結果はJSON形式で標準出力として出力される
$ sudo /var/db/newrelic-infra/newrelic-integrations/bin/nri-flex --verbose --pretty --config_file ./myconfig.yml

例) delayed jobのキュー毎のジョブ数をカウントするメトリックを送信する

delayed jobはRailsアプリで非同期処理を実現するためのRuby gemです。RDBをバックエンドとして利用してジョブの登録や取り出しを行います。今回はこちらのキューに登録されたジョブ数をメトリックとしてNew Relic Flexを使ってNew Relicに送信する実装例を紹介します。

delayed jobキュー毎のジョブ数をRDBに問い合わせて標準出力に表示すれば良いので手段は複数考えられます (DBに接続して直接SQLを発行する、rakeタスクにする等) 。今回は実装を簡単にするため rails runner をシェルスクリプトから実行する方法にします。

#!/bin/bash

# ファイル名は delayed_job.sh とする

cd /path/to/rails_root

bundle exec rails runner '%w(default foo bar baz).each {|q| puts "#{q} #{Delayed::Job.where(queue: q).count}"}'

Rails runnerで実行するスクリプトは以下の内容を一行にまとめたものになっています。

# キュー名 `default`, `foo`, `bar`, `baz` に登録されたジョブ数をカウントして出力する
%w(default foo bar baz).each do |q|
  puts "#{q} #{Delayed::Job.where(queue: q).count}"
end

シェルスクリプトを実行するとキュー名とジョブ数がスペース区切りで出力されます。

default 10
foo 5
bar 0
baz 0

最後に設定用のymlファイルを用意します。さきほど作成したシェルスクリプトのファイルを指定しつつ、メトリック名や実行間隔もymlファイルに書いていきます。New Relic Flexでは得られた出力をどのようにパースするかを指定できます。今回はスペース区切りで出力しているのでそのようにymlファイルにも指定します。正規表現で指定することもできるみたいです。

# /etc/newrelic-infra/integrations.d/delayed_job.yml
integrations:
  - name: nri-flex
    interval: 5m # 実行間隔
    config:
      name: delayedJob
      apis:
        - name:  delayedJob # メトリック名
          commands:
            - run: ./delayed_job.sh # 実行するコマンド (今回はシェルスクリプトを実行)
              split: horizontal # 行指向で分割する
              set_header: [queueType, count] # 項目名
              split_by: \s # データをどのような文字で区切っているか。 \s はスペース区切りを意味している
              timeout: 30000 # タイムアウト (ms)

細かい設定項目については configureexamples を参照ください。

あとは New Relic Infrastructure のプロセスを再起動させれば上記ymlファイルが読み込まれメトリックの送信がはじまります。

メトリックは delayedjobSample という Sample サフィックス付きの名前で保存されます。以下はNRQLで実際に可視化させた例です。

f:id:mizukmb:20210405110818p:plain
New Relic Flexで送信したメトリックをNRQLで可視化した

おわりに

MackerelのカスタムメトリックをNew Relicに移行する時の方法としてNew Relic Flexを紹介しました。どのくらいニーズがあるかわからない内容でしたが参考になれば幸いです。

採用

弥生株式会社ではエンジニアを積極採用中です。

www.yayoi-kk.co.jp