テスト可能なコードを書くということの2つの側面 | try! Swift Tokyo 2017 #tryswiftconf Day2-1 聞き起こし

twitter.com

関数を完全にテスト可能にするためのものが2つあります。作用の分離と共作用を表面化です。この2つの側面の背景にある理論を探り、どのようにすればテスト容易なコードに導けるかを示します。また、最近オープンソース化されたコードベースを基にKickstarterで我々がどのように実践しているかも紹介します。

テスト可能なコードを書くということの2つの側面

テストについて深く話します

f:id:niwatako:20170303100810j:plain

テスト可能なコードを書くには?それは表裏をなすものです。

そして関数言語を例にとって、キックスターターで我々がやっていることをお話します。OSSにしたのでみてもらえます。

Contact Me!

f:id:niwatako:20170303100912j:plain

なぜテストをするか。経験と方法について知ることが必要

f:id:niwatako:20170303100924j:plain

潜在バグを潰せたりして良い。先人たちが色々言っている。

私自身コードを書く時に関数的な書き方で書くことは良い試練だと思う。

実装のためにコードを書くのではなくて、テストはドキュメント化して、アプリケーションがどう動くかの期待値を買いて、エッジケースも考慮に入れていくことが重要。

シンプルな関数でもテストが難しい場合がある。

この関数はディスクからファイルを読んで内容を解釈して数値を返します。

f:id:niwatako:20170303101053j:plain

コンソールに何かプリントすることが要件だとすると、このためにデバッギングをしたりはしない。

かなり簡単に導入できると思います。

f:id:niwatako:20170303101140j:plain

f:id:niwatako:20170303101206j:plain

このようなコードは書いたことがあると思います。ディスクやキャッシュやデフォルトから取得したりすると思います。でもテストする時より考える必要が有ることがあります。

f:id:niwatako:20170303101308j:plain

f:id:niwatako:20170303101312j:plain

f:id:niwatako:20170303101318j:plain

シンプルに見えるがこれだけではすまない。number.txt以上のものが必要でグローバル関数を使う

f:id:niwatako:20170303101352j:plain

String(contentOfFile:) も利用する。これは現在のハードドライブの状態に依存する。

バンドルのパスを取得するGlobal関数はこの関数に関して皆さんが気付いていないような隠れたインプットもあります。

またその結果が他の世界の状態に影響を与えるということが有ります。

この関数がこういうことをしているというのは気が付かないわけです。

f:id:niwatako:20170303101513j:plain

この関数をテストするにはファイルを特定のロケーションに置かなくてはいけないわけです。メッセージがコンソールに出されるということも確認しなければならないです。そしてコンピュータの他の部分には影響を与えないことを確認しなければいけません。

明示的なものと暗黙的なものが入出力にそれぞれあります。それを理解する必要があります

アウトプットの側面

アウトプットのテストが難しいのは副作用の存在です。 実行によって外の世界に観測可能な変化が起きるということです。

テストを実行して、実行したあとの世界の状態を確認する必要があるわけです。

その他の副作用が起きているかを確認するのは難しい。

どのように副作用を扱うか

f:id:niwatako:20170303101722j:plain

副作用をなるべくコードの境界付近、決まったところへ動かしていくところが必要です。

あまり考えていないインタープリタが仕事をしてしまうことになります。

printするのをやめて戻り値を返すことにします。

f:id:niwatako:20170303101800j:plain

この関数の複合がどのように壊れるかということを通常考えなければいけません。

アウトプットは慣れているのは簡単ですが次にインプットに行きます

インプット

完全にコントロールされて明確に定義されたデータ、世界の状態が分からなければなりません。

f:id:niwatako:20170303101942j:plain

共作用によってインプットのテストが難しくなります。

f:id:niwatako:20170303102008j:plain

インプットのテストは共作用のテストになります。

火を使って説明します。

f:id:niwatako:20170303102033j:plain

DependencyのInjectionという人もいるかもしれません。

f:id:niwatako:20170303102051j:plain

ある特定の世界の状態がなければ実行できないということです。

テストのためには特定の世界の状態を作らなければなりません。

やり方は、

f:id:niwatako:20170303102139j:plain

コリンバレットが全てのGlobalを取り入れる、ひとつのstructの中に全て入れてしまうかんきょうにしてしまうという方法を述べています。

f:id:niwatako:20170303102219j:plain

クッキーストレージを入れてしまったり、現在のログインユーザーも入れたり

f:id:niwatako:20170303102241j:plain

日付に対するアクセスは、コードの中で非常に大きな共作用になります。

毎回違う値が帰ってきます。コードの中でDateを使うとそれは純粋な関数ではない。毎回条件が変わってしまいます。

テストの場合にはDateの振る舞いをコントロールするためにプロトコルにしてしまいます。

それからlanguageも

f:id:niwatako:20170303102352j:plain

f:id:niwatako:20170303102404j:plain

Reachabilityも。

次に、ReactiveSwiftを使っています。Schedulerを使ってコントロールしています。信号の遅延などに時間を使う。

UserDefaultsもCo-effectです。このように延々とリストは続きます。

グローバル関数を一箇所にまとめて必ずそこを通すと楽になります。

f:id:niwatako:20170303102540j:plain

成否だけを確認しましょう

f:id:niwatako:20170303102553j:plain

f:id:niwatako:20170303102619j:plain

f:id:niwatako:20170303102634j:plain

f:id:niwatako:20170303102650j:plain

f:id:niwatako:20170303102659j:plain

f:id:niwatako:20170303102714j:plain

純粋な関数になりました。複雑性を関数のシグネチャの中に入れています。これは良いことです。どれぐらいの作業を関数がやっているのかが分かるようになります。

Selfドキュメンテーションという形になります。

ここから結論です。

共作用と副作用でテストが難しくなります。

共作用にたいしてはGlobalな関数に直接アクセスしなくて良いようにする

Q&A

テストに関して気に留めているという話がありました。テストは前によくやるのかあとによくやるのか

テスト駆動でやっています。失敗したテストを書き留めてやるという形になります。

Environmentをチューニングするのにリーダーモナドやステーともなどは使ったりしますか、プロパティベースのUnitTestを行ったりしますか

はい、我々の環境はとても簡単です。モナドなど、ギャップエンバイロメントはCoモナドになっています。あまり考えずにストレートな環境を考えています。プロパティベースはやりたいがやっていないです。