MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

Sourcery を利用して Unit Test のひな形を自動生成する

iOSテスト
February 28, 2022

タクシーアプリ「GO」の iOS アプリを開発している今入です。Unit Test のひな形を自動生成することで Unit Test を書き始めるまでの準備時間を短縮した事例を紹介します。


はじめに

タクシーアプリ「GO」の iOS アプリ開発では Unit Test を書く習慣があります。これは単一の機能の品質担保のためです。「GO」では RIBs アーキテクチャを採用しており、Router および Interactor を対象に Unit Test を作成しています。Unit Test のフレームワークとして QuickNimble を採用し可読性を高くした記述を心がけています。

「JapanTaxi」アプリでの事例ですが「RIBs アーキテクチャにおける Unit Test への取り組み」という記事を以前に書きましたので詳細はこちらをご参考ください。

上記の記事にあるとおり、以前は Xcode Template を利用して Unit Test のひな形を生成していましたが、最近では Sourcery を利用することでより手軽にひな形を生成する仕組みを構築しました。以下にその詳細を紹介します。

Xcode Template の限界

Xcode Template は誰でも容易に扱うことができ、ひな形作成には重宝する機能です。完全に固定化されている記述に対しては Xcode Template で十分ですが、Unit Test の対象となる class などに記述されている定義に従ってひな形を作成することは困難です

Xcode Template を利用していたときのタクシーアプリ「GO」での Interactor の Unit Test の準備は以下のような手順で行われていました。

  • Xcode の「New file」から自作した Unit Test のテンプレートを選択
    • Router, Interactor, Mock のひな形が生成される
  • モック用のひな形から必要な定義のみ有効にする
  • モックが必要な protocol にアノテーションを追加
  • mockolo でモックを作成
  • Viewless RIB の場合は Interactor の Spec ひな形から presenter 関連の処理を削除
  • Interactor の Spec に必要な Mock の定義を追加 & 初期化処理を記述
  • Unit Test 対象の Interactor 自体の初期化処理を記述
  • ビルドが通ることを確認

これらの処理が完了し、ようやく Unit Test が書き始められます。( describe の記述に移ることができます。)

すべてのパターンを網羅する形で Xcode Template にひな形が登録されているため、こういった細かな修正作業が必要となります。あまりにも手間がかかり、Unit Test を書き始めるまでに最低でも数分、場合によっては 10 分以上時間がかかることもありました。せっかく Unit Test を書こうという気になったのに、準備段階で心が折れかけてしまうという事態になりかねません。

ソースコードの解析結果に沿って適切なひな形が作成できれば、こういった煩わしい作業を解消することができます。

Sourcery を利用したひな形の作成

先述した課題は SourceKit や swift-syntax のような静的解析ツールを利用すると解決できます。中でも、既存のソースコードを解析し指定したテンプレートに沿って新規ファイルを作成する場合は、 SourceKit ベースのツールである Sourcery が適しています。

以下に Sourcery における基本的な使い方について紹介します。

既存コードの解析

Homebrew などで Soucery をインストールしましょう。Sourcery の基本的なコマンドは以下となります。

sourcery --sources Interactor.swift \
                  --templates InteractorTemplate.stencil \ 
                  --output InteractorSpec.swift

—sources で解析したい Swift ファイルのパスを指定します。解析された結果は、—templates で指定されたテンプレートファイルに適用した形で —output で指定されたパスに出力されます。

sourcery --sources Interactor.swift \
                  --templates InteractorTemplate.stencil \
                  --output InteractorSpec.swift \
                  --args title=Order,subTitle=PreparingOrder

基本的には Sourcery で解析した結果をテンプレートに適用するのですが、—args という引数を利用すると外部から特定の値を入力させることができます。

その他にもさまざまなオプションが存在するので、詳細は Sourcery の README をご参照ください。

Sourcery の解析結果

Sourcery で解析された結果はどのように表現されるのでしょうか。

本記事では頻繁に利用する Types および Type について紹介します。これらが理解できれば、その他の型については、Sourcery/docs を参照するのが手っ取り早いでしょう。

Types

types を利用することで、解析された class や protocol などの一覧を取得することができます。

types.classes, types.structs, types.protocols, types.enumsといった呼び方で、後述する Stencil テンプレートにおける for ループを利用することで値を取り出すことができます。

Type

Sourcery で解析した結果にはいくつかの型が定義されているのですが、Class, Struct, Protocol, Enum はすべて Type を継承している型となります。つまり Type で定義されているプロパティはすべて利用することが可能です。

たとえば、types.classes の 1 つ目の値 ( class と表現する) に対して、初期化メソッドは class.initializers として取得できます。インスタンスメソッドは class.instanceMethods, プロパティは class.instanceVariables といった形で同様に取得できます。

Type にはさまざまなプロパティが用意されているので、その詳細は Sourcery/docs/ をご参照ください。

この構成について理解があれば、次に紹介する Stencil テンプレートへの適用もスムーズに行えると思います。

Stencil テンプレート

Stencil と呼ばれる Swift 用のテンプレート言語があり、Sourcery のテンプレートとして採用されています。Sourcery で解析した結果は .stencil ファイルとして記述されたテンプレートに沿って出力されます。

以下に、タクシーアプリ「GO」での実例を交えながら、その使い方を紹介します。

外部から与えた引数を適用する

{% for key, value in argument %} // ① 引数一覧を取得
{% if key == "RIB" %} // ② 引数の key が「RIB」だった場合
final class {{ value }}InteractorSpec: QuickSpec { // value に外部から指定した情報を代入
    // RIBs components
    var router: {{ value }}RoutingTestingMock! // var router: SampleRoutingTestingMock!
    var interactor: {{ value }}Interactor! // var router: SampleRoutingInteractor!
{% endif %} // ② の条件処理の終わり
{% endfor %} // ① の繰り返し処理の終わり

Stencil テンプレートでは {% %} で囲まれた部分は、テンプレートエンジン内で処理され何も出力されません。逆にそれ以外の部分は空白や改行を含めすべて記述されているとおりに出力されます。

先述した Sourcery コマンドにて —args を利用した場合の例です。—args RIB=Sample,title=Test として実行した場合、上記の key, value として「RIB,Sample」と「title,Test」といった情報が取得できます。今回の場合は、key が RIB だった場合のみテンプレートが適用されるように記述されています。

Sourcery で解析された結果以外に外部から情報を与えたいときは —args を利用するとよいでしょう。

特定の class を継承しているかどうかの判断

{% for class in types.classes %} // ① class 一覧を取得
{% for inheritedType in class.inheritedTypes %} // ② 継承している型, 準拠している型の一覧を取得
{% if inheritedType|contains:"PresentableInteractor" %} // PresentableInteractor を継承している場合
    var presenter: {{ value }}PresentableTestingMock!
{% endif %}
{% endfor %} // ② の繰り返し処理の終わり
{% endfor %} // ① の繰り返し処理の終わり

class.inheritedTypes を利用して、継承している型一覧を取得し、その中から特定の文字列が含まれる型があった場合のみ presenter というプロパティを定義するテンプレートになります。

contains を利用すると条件式で特定の文字列が含まれるかどうかが判断できます。

初期化時の引数から特定の型名を取得する

{% for class in types.classes %} // ① class 一覧を取得
{% for method in class.initializers %} // ② class の初期化メソッドを取得
{% for methodParameter in method.parameters %} // ③ 初期化メソッド内の引数を取得
{% if methodParameter.typeName|hasSuffix:"Stream" %} // ④ メソッドの型名の末尾が「Stream」の場合
{% if methodParameter.typeName|hasPrefix:"Mutable" %} // ⑤ メソッドの型名の文頭が「Mutable」の場合
    var {{ methodParameter.name }}: {{ methodParameter.typeName }}TestingMock! // var sampleStream: MutableSampleTestingMock!
{% endif %} // ⑤ の条件処理の終わり
{% endif %} // ④ の条件処理の終わり
{% endfor %} // ③ の繰り返し処理の終わり
{% endfor %} // ② の繰り返し処理の終わり
{% endfor %} // ① の繰り返し処理の終わり

少々読みづらいですが、上記のように Stencil テンプレートを記述すると、初期化時の引数一覧から「 Mutable で始まり Stream で終わる」型を取得し、それに対するプロパティの宣言を行っています。

MethodParameter 型に定義されている name でその引数名を、typeName でその型名を取得できます。条件式の中で | を追加し hasSuffixhasPrefix を利用して、末尾と文頭が合致するかどうかを判定しています。

特定のプロパティ以外は Xcode のプレースホルダを入力させる

{% if method.typeName|hasSuffix:"Stream" or method.typeName|hasSuffix:"Repository" or method.name == "presenter" %}
    {{ method.name }}: {{ method.name }}
{% else %}
    {{ method.name }}: <#T##{{ method.typeName }}##{{ method.typeName }}#>
{% endif %}

これまでに紹介したものを組み合わせたものなので真新しさはありませんが、上記のように条件式に or を利用することで複数の条件を扱うことができます。

Sourcery で解析した結果を元に Unit Test のひな形を作成してきましたが、Sourcery に解析させるファイルに不足があったりその他の要因で自動的にひな形を作ることが困難な場合もありえます。

その際は、無理に自動化するのではなく人間の手で補完すればよいと思い、Xcode のプレースホルダを適用させるようにしました。Xcode におけるプレースホルダは <#T##_A_##_B_#> のように表現することができ、_A_ が入力前に表示される内容で _B_ が Enter キーを押した際に自動補完される内容となります。

Swift Package Manager として Unit Test のひな形自動生成ツールを作成する

これまで紹介したとおり、既存のソースコードを Sourcery を利用して解析し、Stencil テンプレートに沿った形で Unit Test のひな形を出力させることができるようになりました。これだけでも十分手間が省けて問題が解消されたのですが、まだひと手間残っていることがあります。それは既存ファイルの修正です。

タクシーアプリ「GO」ではモック生成に mockolo というツールを利用しているのですが、モック生成対象の protocol を特定のファイルにまとめて記述しています。これは本番コード内に Unit Test 用のアノテーションを付与させないという目的のためです。

冒頭で紹介したとおり、Unit Test の準備段階にて、新たに必要となるモックに関する定義を追加しなければなりません。これは既存ファイルに対する修正となり、新規ファイル作成に特化した Sourcery だけでは対応することができません。

そこで、既存のファイルに必要な情報を追記させる処理を Swift Package Manager として作成することにしました。最終的に Sourcery によるひな形自動生成やその他バリデーションチェックなどを含めた形で SPM が完成されています。この SPM では以下の処理を行います。

  • Unit Test 対象のファイルの存在有無のチェック
  • Stencil テンプレートファイルの存在有無のチェック
  • Unit Test のひな形出力先の存在有無のチェック
    • すでに Unit Test が存在している場合はひな形生成を行わない
  • Sourcery のインストール有無のチェック
  • Sourcery でモック対象候補を解析し一時ファイルとして出力
  • 新たに必要となるモックに関する定義を追記
  • Sourcery を利用し、対象の Router, Interactor の Unit Test ひな形を作成
  • mockolo で最新のモックファイルを生成

太字で示している「新たに必要となるモックに関する定義を追記」が Sourcery だけでは実現できなかった処理です。SPM から直接 Swift ファイルを操作することにより既存ファイルの書き換え(追記)を行っています。それに必要な情報は Sourcery の解析結果を利用することができました。

SPM 化したことで、Sourcery で入力する値のバリデーションであったり不必要にひな形を作成してしまうミスを回避したり、ユーザビリティが高い形で完成させることができました

さらに、このツールを make コマンドで扱えるようにしたことで、タクシーアプリ「GO」の Unit Test のひな形は以下のコマンドを実行するだけで自動的に生成できるようになりました。

make unit_test RIB=Sample

Unit Test を書きたいコンポーネントの名前を指定すれば、自動的に既存のコードを解析し Unit Test のひな形が生成され、Xcode を開けばすぐに describe 配下のテストコードが記述できるようになりました。Xcode Template を利用していたときに比べ、Unit Test 作成時の準備の手間が大幅に省けていることが分かります。

おわりに

Unit Test を書くまでにはいくつもの壁が存在します。モックの準備であったり、本記事で紹介したようなひな形の準備であったり。

「Unit Test を書きたいなぁ」と思ったのもつかの間、「でもあまり時間はないし、書き始めるまで色々と面倒だし……」といった迷いが心の中で浮かび上がってきます。Unit Test を書きたいというモチベーションを持続させるためにも、煩わしい準備作業は自動化することが望ましいと思います

本記事が Sourcery を初めて利用する際の手助けになったり、ひな形作成の自動化についてのインスピレーションが湧くきっかけになれば幸いです。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @mot_techtalk のフォローもよろしくお願いします!