Artsyには4つのiOSアプリがあり、すべてオープンソースでそれぞれテストへのアプローチが異なります。なぜでしょうか。それは異なるテスト方法は状況によって良くも悪くも機能するからです。try! Swiftの講演から、すばらしいソフトウェアを開発するためにいつ、なぜテストが重要かをよく理解できるよう、ArtsyのiOSチームが決断した背景にある動機、直面した苦労、課題をどのように克服したかについてお話しします。
Artsyでのテスト (0:00)
Ashです。ニューヨークにある会社、Artsyでオープンソースのエンジニアをしています。私たちは芸術を音楽と同じくらい人気のあるものにしようとしています。私は主にiOSアプリを担当しています。
Artsyでは4つのiOSアプリを開発してそれぞれ異なる単体テストへのアプローチを行いました。何が機能して何が機能しないか、また状況によって異なる機能の良し悪しを経験して多くのことを学びました。
Artsyの4つのアプリの単体テストの過程を説明していきます。2つのアプリはObjective-Cで開発しましたが、ここでお話しする内容はSwiftにも適用できます。この講演を通じて3つのことを念頭に置いていただきたいです。
- 完璧でなくても構いません。テストカバレージが100%でなくても構いません。本来テストは役に立つものですが、単体テストをすること自体に価値があります。
- 既存の部品をできるだけ小さくします。小さいほうがテストしやすいからです。
- 新しいアプリではできるだけ、ばかばかしいほど小さな部品にしておきます。どのくらい小さいかをお話しします。
Emergenceのテスト (1:36)
こちらが最初のアプリでコードネームはEmergenceです。Apple TVに対象をしぼって提供しています。アプリを起動して都市を選択すればどんな展示会があるのかわかります。たとえば東京を選べば東京でどんな展示会があるのかがわかります。
テストの方針を見てみましょう。
¯\_(ツ)_/¯
うーん、テストがありません。
テストがないアプリで単体テストの講演を始めるのは少し変わっていますが、重要な理由が2つあります。1つめは、テストできないときまたはテストすべきでないときがあるからです。2つめは、みなさんの中でテストが重要ではないと考えておられる方からの信頼を得たいからです。
私たちのコミュニティでは、場合によってはテストは新しく、そして難しく感じて自信をなくすことがあり、テストする時間がないときがありますね。それは構わないと思います。
なぜこのアプリでテストをしなかったかをお話ししたいと思います。
- まずこのアプリは1か月未満で開発しました。Apple TVの発売日に必要だったので納期がとても厳しかったのです。
- マンチェスターにいる同僚のOrtaが1人で1日中開発していました。機能を追加せずに最小限でメンテナンスしています。それだけで完成させました。
- このアプリは対象をしぼって1つのことをしっかりやっています。
テストをするかどうかはソフトウェア開発のようにバランスを図るものです。どのチームもどのメンバーもそのバランスを意識して決定します。Emergenceはとても小さなアプリです。同僚に尋ねると2週間以内で一から作り直せると言いました。ただ急いで実装する必要があったので、時間を優先してテストを追加しませんでした。
さてまとめますと、テストはすべきでしょうか。おそらくそうでしょう。しかしそれは必須ではありません。またできなくても責めるべきではありません。
Energyのテスト (4:00)
次に、Energyというアプリについてお話しします。こちらは画廊向けのアプリです。このアプリは在庫を管理して見込み客に見せます。実は一番古いアプリで最初のコードベースです。もともとはテストなしで開発してあとから追加しました。それは意識的な決断でした。なぜテストを追加すると決めたのでしょうか。
なぜテストを追加するのか? (4:53)
EnergyのコードベースはEmergenceよりかなり大きいのでより壊れやすいです。コードとAPIの同期がとても重要です。壊してはいけない重要なコードです。バグがあってはいけないのでそれを確認するためにテストしました。
何よりもBus Factorを減らしたかったのです。Bus Factorとはチームの個々のメンバーにどれぐらい知識が集中しているかを表す値です。何人がバスにひかれてしまったら製品開発/ビジネスが立ちゆかなくなるのかを表します。
テストを追加したのは、同僚の頭の中で培われた組織として持つべき知識をドキュメント化するためです。単体テストはアプリがどのように機能するかをドキュメント化します。よいテストはアプリの挙動が明確になっています。ただしクラスの内部ではなく外部の挙動だけです。
クラスをとても小さくするように言うと反対する人もいます。そうするとクラスが増えるからです。すべてのクラスが小さくて、組み合わせ可能で、しっかりとテストされているなんてとても恐ろしいですね!
Dependency Injection (6:19)
Energyは私たちのiOSアプリの中で一番よい単体テストを備えています。何がよかったのでしょうか。よくDependency Injectionを使います。Dependency Injectionはオブジェクトが依存するものを自分自身では作らないという考えです。代わりにそれらを外部から注入します。
Core Dataのようなシングルトンオブジェクトに直接アクセスするかわりに、Core Dataへのアクセスを管理するオブジェクトコンテキストをわたすことができます。また、Dependency Injectionをネットワーク同期するコードのスタブにも使います。Energyはテストの際にはすばやく低コストで作成できるインメモリの管理オブジェクトコンテキストを使い、テスト終了後に破棄します。テストの一部はオブジェクトがどのようにCore Dataストアを変更するかをテストします。管理オブジェクトコンテキストを作成してテスト対象のオブジェクトに注入し、処理が終わったあとで期待した通りに変更されているかをチェックします。
RSpecでテストの視覚化 (7:38)
はじめに、それぞれことなる3つのテストがありました。それらが何をしているかを知るためには大量のコードを読む必要があります。重複したコードもあったと思いますが誰にもわかりません。
この状況を改善するため、RSpecスタイルのテストを取り入れていきます。RSpecという用語はRubyのコミュニティから来ています。iOS向けのKiwiやSpecta、またはQuickのようなライブラリを使うことができます。RSpecスタイルのテストを使うにはテストに目を通して共通の設定を特定し���いきます。一度そうすれば1か所だけでリファクタリングができます。
それぞれのテストに記述されていた共通のセットアップコード���一旦削除し、それぞれのテスト前に実行されるbefore eachに移動しました。Energyでは、よくインメモリCore Dataストアを作成し、テストコードでそれを利用します。RSpecスタイルのテストの美しさは、この方法を繰り返しおこなうことでさらに共通のセットアップコードを見つけることができるという点です。
2つの最も内側のテストは内部と外部のbefore eachに任すことができます。たとえばもし外部のbefore eachが管理オブジェクトコンテキストを作成する場合は、内部のbefore eachでテストデータを追加してもよいかもしれません。さて、何を始めたかを見てみましょう。3つのテスト、それらが何をするか、どのくらい複雑か、それぞれのテストを読まないとわかりません。それを知るにはそれぞれのテストを読む必要があり時間がかかります。それはうんざりしますしもっとよいことに時間を使いましょう。
対照的に、新しいテストは読みやすいです。読み書きするコードが減り、重要なことは単体テストはテストしていないコードをできるだけ明らかにすべきだということです。見た目でどれくらいのコンテキストがテストされているか、どれくらいネストしているか、それぞれのテストがどれくらい複雑であるかが相対的な長さでわかります。コンテキストがネストしすぎている場合はおそらく複雑すぎるのです。
より重要なことは、内部と外部のコンテキストを作成してわかりやすい名前をつけることができるということで、これにより失敗したテストを簡単に特定できます。Xcodeのエラーメッセージは”In a Core Data store with sample data, deleting objects failed.”のような感じになります。
Energyのまとめ (10:56)
まとめると、テストはアプリやアプリを構成する部品のドキュメントとして機能します。オブジェクトの挙動をテストすべきであって内部をテストすべきではありません。もしその挙動が複雑すぎて内部に手をつけないとテストできないのであれば、コードベースを見直します。私の意見では、それぞれのクラスのPublic Functionは1つだけであるのが理想です。
Dependency InjectionはネットワークやCore Dataを使う部分にモックを使ってテストを書きやすくします。RSpecスタイルのテストは簡潔でわかりやすくなります。大切なので繰り返しますが、単体テストによってテストしていないコードがより明らかになります。
Eigenのテスト (11:50)
次は、Eigenです。ぜひiPhoneとiPadにインストールして芸術を探っていただきたいコンシューマ向けのアプリです。
はじめは単体テストなしで開発してあとから追加しました。3名のエンジニアで急いで終わらせたやっつけ仕事でした。着手時に時間がなかっただけでEnergyでテストの価値がわかっていたのであとからテストを追加しました。
テストの課題 (12:42)
Eigenはおそらくみなさんがテストするであろうもっとも典型的なアプリです。多くのコードがからまりあって多くのエンジニアが異なるコーディングスタイルで書きました。このアプリのテストを始めるのには苦労しました。
苦労した大きな理由のひとつは、ネットワークにアクセスする部分が1箇所だったEnergyと比べ、Eigenではコードのあちこちに散らばっていました。ネットワークのレスポンスを一つ残らずモック化できていることを保証することがEigenをテストしようとしていたときの大きな問題でした。現在も継続してテストを追加していますが、その際にも多くのエンジニアが担当するので統一されたスタイルでコードを記述することはとても大変です。
比較的少数のエンジニアで開発し統制が取れていたEnergyとは対照的で、異なったスタイルでコーディングされているためテスト手法もそれに合わせて異なったものになり、余計な時間がかかってしまいます。
マルチプラットフォームテスト (13:35)
EigenはiPhoneだけで取り掛かりましたが結局はユニバーサルアプリになり、マルチプラットフォームでどのようにテストするか疑問が生じました。いくつかの選択肢がありますがShared Examplesという方法を使いました。
Shared Examplesはコンテキストを与えてテストを定義します。テストは数回異なるコンテキストで実行します。この場合は2つのコンテキストで、つまりiPhoneとiPadでテストを通します。
テストを追加していた当初、一番大きなクラスを最初にテストしようとしました。もっとも不安定で最優先でテストすべきと考えたからです。しかしとても大きいので内部をテストする必要がありました。しかしそのとき内部を変更するのはとても難しいのです。このクラスを変更するためには多くのテストも更新する必要があるからです。
私たちの全体ルールはコードベースに新たなコードを追加したら必ずテストすることです。もし既存コードを変更する時間があるのならテストの追加もまた同じように優先させます。バグの修正には必ずテストの追加が必要です。後戻りとバグの再発を防ぐためです。
スナップショットテスト (15:19)
iOSアプリのテストでお気に入りの方法の1つはスナップショットテストを使うことです。それにはFacebookが公開しているとてもすばらしいライブラリを使い、以下のように機能します。
ビューまたはビューコントローラを用意してデータを渡しビューがどう見えるかの画像をリポジトリ内にPNGで保存しておきます。あとでテストを再実行したとき同じ設定で同じデータを渡し、別のスナップショットとピクセルごとに比較します。もし違っていればテストは失敗します。
こちらはとても便利です。PNGを保存していくのでリポジトリのサイズが大きくなっていくデメリットはありますが、うっかりユーザーインターフェースを変更してしまうのを検知するすばらしい方法です。
またPull requestをレビューするときにとても便利です。たとえばインターフェースがどのように変更されるか視覚的に確認でき、レビューがとても簡単になります。
支援を得る (15:31)
場合によってはやり方がわからず行き詰まってほかのiOSエンジニアに相談しま���。おそらくブログに書いたりGitHubのIssueやPull requestのリンクをツイートしたりするでしょう。アプリをオープンソースにすることでそうしやすくなります。もし本当に行き詰まったらそれが理想でなくても何らかの解決策を講じて、どのように改良したいか記録してみてください。そうすればあとで戻ったときに何をすべきかがわかります。覚えてください。完全なものはありませんし完璧なものはありません。それで構いません。
Eigenのまとめ (17:26)
まとめると、テストを書きはじめたときは分解しなかったのでコストがかかっていました。今はクラスを小さくし、コードをシンプルにしてテストが簡単になるようにしています。もしあなたがテストの方法を学んでいるところであったり不慣れなのであれば、一番小さなクラスから始めることをおすすめします。そうすれば単体テストに慣れることができます。
チームで新しいコードは必ずテストするというルールを作り、新たに変更されたコードにはテストを追加してください。ときには行き詰まりますがそれで構いません。すばらしくとても役に立つiOSコミュニティに助けを求めることができます。場合によっては理想的または完璧ではないコードを書かなければならないですが、なぜそう決断したのか、今後どのように修正したいかを記録する努力をします。
Eidolonのテスト (18:33)
アプリのテストをどのように書くかをはじめからお話ししましょう。こちらはEidolonで、オークションで芸術作品に入札するためのアプリです。社内向けのEnterprise Distributionを使ってiPadに配布しています。オークションで芸術作品を閲覧して入札できます。
このアプリはもともとテストを含めて開発しましたが、プロジェクトの終わりに時間が切迫していたので近道をする必要がありました。そうして技術的負債を抱えていましたが、アプリをメンテナンスしていくうちにテストを追加することで技術的負債を減らしやすくなりました。
Swiftのテスト (19:20)
こちらは初めてSwiftで開発したアプリで、Swift1.0がまだベータ版である時期に開発を始めました。言語、コンパイラ、Xcode、それらすべてが変わっていく中で単体テストをすることでツールや言語への理解を確認することができました。またツールや言語の更新の際にアプリが壊れないように気をつけました。
初めてのSwiftアプリなので、単体テストのみが唯一なじみだと感じることができるものだったというところがおもしろいです。テストをすることでソフトウェアエンジニアリングの原則を守りやすくなります。Swiftがどのように機能するか理解し、コードの品質に自信を持つことを気をつけるようになりました。
Quickを使う (20:10)
QuickはRSpecスタイルのテストのためのライブラリでSwiftとObjective-Cを使っています。よいテストを書くために開発中からQuickを使い、Quickチームにフィードバックを送りました。彼らがいろいろ質問に答えてくれたのでアプリの開発とライブラリのテストを同時にすることができてとても有益でした。
QuickはRSpecスタイルのテストフレームワークを、Nimbleはそのうえでとてもすばらしいマッチャを提供してくれました。マッチャとは何でしょうか。よい質問です。実際にテストがどのようなものかよく見てみましょう。
Eidolonでのよいテストは短く、また一般的にもよいテストは短いと言えるでしょう。Arrange(環境構築)、Act(実行)、Assert(動作確認)の3つがあります。
- Arrangeはbefore eachでほとんど済みます。
- Actはテストするメソッドを呼びます。
- Assertはクラスの挙動が期待通りであるかをテストし、エクスペクテーションを呼びます。
場合によってはアサーションを呼びます。アサーションとエクスペクテーションは本質的には同じものです。さてエクスペクテーションはどのようなものでしょうか。
Nimbleのパターンマッチング (21:46)
Nimbleの使い方を知るために、まずはXCTestのアサーションを見てみましょう。
XCTAssertEqual(1 + 1, 2, "...")
長いですね。今度はNimbleのマッチャを使ってみましょう。
expect(1 + 1).to( equal(2) )
より簡潔でわかりやすいですね。カスタムオペレータをオーバーロードしてさらに簡潔にできます。
expect(1 + 1) == 2
私はこのようなテストが好きです。アサーションの代わりにエクスペクテーションを呼びます。しかし本質的には同じものだと覚えてください。NimbleはString、Array、Range、すべてのEquatableな型のマッチャを備えています。
XCTestを使わずにとても簡単に非同期コードをテストできるマッチャもあります。そして先にお話ししたスナップショットライブラリでNimbleを使ってカスタムエクスペクテーションを処理するカスタムマッチャを書くこともできます。
Eidolonのまとめ (23:11)
まとめると、どのように効果的なテストを書けばよいかということです。Quickを使ってRSpecスタイルに、Nimbleのマッチャを使ってわかりやすく短いテストを書きます。テストをわかりやすく保つためにカスタムマッチャを書き、テストあたり1つのエクスペクテーションに制限してみます。テストは短く、わかりやすく、十分にあるべきです。
テストのおさらい (23:40)
講演のはじめに念頭に置くようお願いした3つの概念をおさらいしましょう。
-
みなさんには完全でなく、完成させなくても構わないと言いました。完全なもの、完成したものなどないからです。場合によっては理想的でないコードを書かなければならないですがそれは構いません。イライラしたり問題に行き詰まってもそれは普通のことです。iOSコミュニティから支援を得ることができるのでとても有益です。
-
既存の部品を小さくしてみましょう。テストしやすくメンテナンスしやすくなります。そしてクラスのPublic Interfaceだけをテストして内部はテストしません。ところでPublic Interfaceはとても小さくあるべきです。1つが理想です。新しいアプリでははじめから小さく保つようにします。明確に定義されたロジックとコーディングスタイルはテストしやすいです。ただ必ずしもそうできるわけではありません。
-
クラスを小さくすることで、異なるコーディングスタイルの差をわずかなものに保ちます。Eidolonのクラスを小さく保つことで技術的負債を最小限にし、あとでそれを返済しやすくなりました。
すべてのiOSアプリはオープンソースでエンジニアのみなさんができるだけ簡単に実行できるようがんばりました。ぜひ確認してみてください。そしてどのように開発したのか、どのようにテストしたのかについて質問があればすぐにGitHubのIssueを開いてください。
Q&A (25:03)
Q: サーバサイドのチームメイトへはテストの重要性をどのように説明していますか?
Ash: この講演を通してテストのメリットについて話そうとしました。アプリの挙動をドキュメント化することや、重大なコードは壊さないように気をつけることです。iOSアプリでのテストは少なくともサーバサイドと同じくらい重要だと言うでしょう。たとえばRailsアプリケーションの配布はApp Storeのレビューを通さなくてよいですが、もしテストせずに単体テストで見つけることができていたはずのバグをそのままリリースしてしまったら、レビューのために1週間バグを抱えたままあなたの顧客を待たせてしまいます。
Q: UIテストや結合テストについてはどのような見解をお持ちですか?
Ash: それはまだ決めかねています。主な理由はUIテストがXcodeに追加された最近までツールに感動していなかったからです。率直に言うと入念に調べてはいません。ビューとビューコントローラをとても薄くすればスナップショットテストで十分です。ユーザーとの対話に応答したりデータを表示したりするだけで多くのことはしません。そして別のクラスにロジックやものを持たせればそのクラスを独立してテストできます。
だから隅々までテストはしませんが、そうしなくても多くのものが機能します。車を火星に送って隅々までテストすることはできません。分離してテストする必要があります。NASAにとってそれでよいのであれば私たちもそれでよいのです。
単に私の意見ですが、人には好みがあります。UIテストがチームやあなたにとってうまく機能するのであれば全力を尽くしてください。
Q: テスト駆動開発を始めること、必要とされる思考プロセスについてのアドバイスはありますか?
Ash: テスト駆動開発は論争を呼びます。ある人はとても好きですし、ある人はとても嫌いです。私は中間です。コードを書くのが好きでたまらないのでよく実装だけしてあとでテストします。そうすべきでないとわかっていますが。
コードを書いていてテストを書き始めるとテストするのが難しいクラスを書いていたことがわかって変更することがあります。それを何度も繰り返すうちに簡単にテストできるようになります。そういった経験がテスト駆動開発と同じくらい役に立つ方法だと思います。
同じくらい強力なアプローチではないですが、チームにとってバランスとトレードオフを考えて、TDDをしないだけの価値があるのでそれでよいです。
アドバイスとしてはクラスを小さく保つことです。テストが大変であればクラスを分割してDependency Injectionを使ってそれぞれをテストしてみてください。そして実践するだけです。経験することでよくなっていくでしょう。
翻訳: JPMartha
About the content
2017年3月のtry! Swift Tokyoの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。