Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

TestPlanの使い道 #testplan #xcode #hakata_test_night

f:id:fromkk:20200122115029p:plain

一昨年の12月に博多に来て以来、丸一年ぶりに博多に来ています。
目的はSwift Days Fukuokaに参加するためです。

Swift Days Fukuokaとは

全国のSwiftに何らかの関係があるエンジニアが、福岡に一斉に集まって情報共有をする3日間(1/24 ~ 26)のイベントです。

第11回 HAKATA.swift x Swift愛好会~福岡と東京のSwift勉強会コラボ~ - connpassより引用

初日の今日は testnight.connpass.com です!
イベントが発表されてすぐ登壇申し込みをしたんですが、特に発表する内容を考えていなかった中頑張って捻り出したのがTest Planの使い道です。

発表資料

Test Planとは

Xcode 11から追加されたテストに関する新たな機能です。
これまでは端末の状態や設定を変更してテストをしようとすると都度スキームの設定を変更する必要がありました。
そういった場合にCIなどで柔軟なテストを実行しようとするとxcodebuildコマンドで頑張る必要がありました。
例) -testLanguage-testRegionオプション
Test Planを利用するとそういった設定を一つのJSONファイル(.xctestplan)に書いておき実行時にそれらをまとめて実行することができるようになりました。

参考: Testing in Xcode - WWDC 2019 413

Test Planの設定方法

Schemeの編集画面のサイドバーでTestを選択し、左下の ボタンからTest Planを追加します。
既にTest Plan未対応のテストが既にある場合は Convert to Test Plans というボタンから変換も可能です。

ターゲットの追加

f:id:fromkk:20200122120822p:plain

続いてテストするターゲットを追加します。

f:id:fromkk:20200122121158p:plain f:id:fromkk:20200122121219p:plain

これでターゲットの追加が完了です。 Configurationを選択してみます。

f:id:fromkk:20200122121427p:plain

全てのテストで共通の設定がある場合はShared Settingsを設定します。
左下の +ーボタンから構成を追加・削除することができます。 構成を追加するとShared Settingsで設定した項目が継承されながら独自の設定が可能になります。

設定項目

f:id:fromkk:20200122121639p:plain

テスト実行時の設定を変更することができます。(引数、環境変数、言語や地域など) また、テストの実行順序やコードカバレッジ、クラッシュ時に関する情報の取得も可能です。

テストの実行

⌘(Command)+Uで選択中の全てのテストが実行されます。
複数のConfigurationが存在する場合でテスト実行ボタンをCtrlキーを押しながらクリックすると特定のConfigurationを選んで実行することができます。

 f:id:fromkk:20200122124542p:plain

コマンドラインからはxcodebuild test -testPlan 'TestPlan名'でTest Planを指定して実行することができます。

更に詳細については弊社の@akatsuki174さんが書いた記事も参考にしてください。 qiita.com

本題

ようやくここからが本題です。
なんとなくTest Planはよさそうですが、実際にどういう時に利用するのが有効なのでしょうか。
ここでは2つのテストケースでTest Planを利用することで不具合が見つけられそうというものを考えてみました。

翻訳が正しく設定されているか

と言ってみたものの現状のTest Planで大きく活躍しそうなのは言語と地域にまつわるものかと思います。
前提としてこのような見た目のUIがあるとします。

f:id:fromkk:20200122125701p:plain

(上も下も「登録」になるようなUIは実際には無いと思いますが一旦進めます)

まずは日本語だけでテストをしてみます。

f:id:fromkk:20200122125759p:plain

テストコードはこんな感じにしてみました。
無事テストが通っていることがわかります。

f:id:fromkk:20200122134756p:plain

ここでTest Planに英語設定を追加してみます。

f:id:fromkk:20200122134823p:plain

テストが失敗しました。

f:id:fromkk:20200122134950p:plain

UIを作っていたところのコードの翻訳キーを見ると RegisterRegistration になっていましたね。
テストを修正して再実行してみます。
成功しましたね。

f:id:fromkk:20200122135057p:plain

偏った環境でだけテストをしてうまくいっていても、新たな環境を追加することで失敗することがあるかもしれないのでこういった場合にTest Planは有効かもしれません。

Localeの未設定を見つける

日付の単位を言語毎に出し分けしたいことがあったとします。
日本語だと数値をそのまま表示して、英語の場合は、1st, 2nd, 3rd...のような表記にしたいということです。
下記のようなコードがあるとします。
引数は数値の日付と言語を受け取って String の値を返します。

import Foundation

struct DayConverter {
    static func convert(day: Int, for languageCode: String) -> String {
        if languageCode == "en" {
            let day = day
            let number = NSNumber(value: day)
            let numberFormatter = NumberFormatter()
            numberFormatter.numberStyle = .ordinal
            return numberFormatter.string(from: number)!
        } else {
            return String(day)
        }
    }
}

英語の場合に正しい値を返してもらえれば良いのでまずは英語でのテストをしてみます。

f:id:fromkk:20200122135929p:plain

問題なさそうですね。

f:id:fromkk:20200122135950p:plain

それでは日本語を追加してテストしてみます。

f:id:fromkk:20200122140010p:plain

失敗しましたね。 is not equal to ("第XX") となっているのが分かります。

f:id:fromkk:20200122140403p:plain

元々のコードを修正してみます。

import Foundation

struct DayConverter {
-    static func convert(day: Int, for languageCode: String) -> String {
+    static func convert(day: Int, for locale: Locale) -> String {
-        if languageCode == "en" {
+        if locale.languageCode == "en" {
            let day = day
            let number = NSNumber(value: day)
            let numberFormatter = NumberFormatter()
+            numberFormatter.locale = locale
            numberFormatter.numberStyle = .ordinal
            return numberFormatter.string(from: number)!
        } else {
            return String(day)
        }
    }
}

これで無事テストが通りました。

f:id:fromkk:20200122140644p:plain

この場合は実は英語、日本語だけではなく Locale が持っている Region 情報が大事でした。
そしてほとんどの locale プロパティは特に値を渡さないと現在の設定をそのまま利用するので、設定忘れがあるとこの例のように複数環境でテストをする際に失敗することがあります。

一連の流れを動画にしましたので良ければご覧ください。

Pros/Cons

ちなみに弊社で開発・運用しているアプリケーションのFammでもTest Planを活用しています。
導入する中で1つ不具合を見つけることができたのですがそれもTest Planのお陰でした。

TestPlanを導入するデメリットとしてはテストの実行回数が増えるので単にテストにかかる時間が伸びてしまうことですね。
このあたりは普段使いするTest PlanとCI上で実行するTest Planを分けるなどして運用を設計するのがよいでしょうか。

まとめ

Test Planが出るまでは環境を動的に変更したり複数環境でテストをすることは大変だったので、Test Planのお陰でそういったテストが簡単に実行できるようになりました。
特に必要なDIを忘れていることなどが気付きやすくなったので、是非活用して更に品質の高いプロダクトを作っていきましょう。

P.S.

初日は福岡についてすぐとんかつを食べたり、牛タン・焼肉を食べました。

f:id:fromkk:20200124195307j:plain

f:id:fromkk:20200124195345j:plain

f:id:fromkk:20200124195410j:plain

福岡らしいものを何も食べてない!!
明日は

cswift.connpass.com

に参加して

hakata-swift.connpass.com

で登壇します。楽しみ!

積極採用中!!

子育て家族アプリFammを運営するTimers inc.では、現在エンジニアを積極採用中!
急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP: http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる