サナギわさわさ.json

サナギさんとキルミーベイベーとプログラミングが好きです

「快感回路」を読んだ

最近インプットが減っていて良くないと思ったので、色々な所で紹介されていた「快感回路」という本を読みました。

快感回路---なぜ気持ちいいのか なぜやめられないのか (河出文庫)

快感回路---なぜ気持ちいいのか なぜやめられないのか (河出文庫)

当初想定していたよりも大分科学的な内容で驚きましたが、内容自体は面白く、かなり楽しめました。明日すぐに仕事に使えるとかそういうタイプの本ではありませんが、こういう知識を取り入れておく事で、例えばサービスを成長させるための施策の質が上がったりするといいなあと思います。

以下、各章の感想と紹介です。

第1章 快感回路の発見

ラットの快感回路を用いた実験で、ラットが自分の脳を刺激するために1時間に7000回のペースでレバーを押し続けたというのはゾッとする。同様の実験を人に対して行った際の描写は更に衝撃的。

患者は、最も多いときには一日中、自分の健康も家族のことも気に掛けずに自分を刺激し続けた。(中略) ときには、装置を遠ざけてくれと家族に懇願し、取り上げられてしばらくすると必ず、返してくれと要求した。

個人的には、快感という現象がVTA(腹側被蓋野)のドーパミンニューロンの活性化によって科学的に説明できるというのが割と驚きで、人の心は科学的に解明できていないというのは固定観念が覆された。

また、私たちが生き続けるためには食べたり飲んだりセックスしたりといった経験を快いものとして感じる必要があり、快感回路がその仕組みの構築に寄与しているという話も面白い。(極めて原初的な線虫でも基礎的な快感回路を持っており、その回路を破壊すると食事をとらなくなるらしい)

第2章 やめられない薬

古代ローマにおけるアヘン、19世紀アイルランドにおけるエーテルなど、どんな時代においても人類は自らの脳の機能を変容させる手段を見つけ出し、統治者はその利用を規制してきたが、人間だけでなく、野生動物も精神的活性効果を持つ植物や菌類を習慣的に喜んで口にするというのは興味深い。 最も分かりやすい例は栄養にならないベニテングタケを食べて酔う家畜化されたトナカイらしい。こういう雑学を隙あらば繰り出していけるような人間になりたい。

また、向精神薬の中でも快感回路を活性化するものとしないものがあり、それによって依存症リスクの大きさが異なるという話は納得できた。摂取方法(脳のニューロンに到達するまでの早さ)・入手難易度・社会的な位置付けなどが依存症リスクを左右するという説明も面白く、タバコの方がコカインより依存症発生率が高いという事実への回答になっている。

また、一般的に依存症は心の弱さが原因と受け取られる事が多いように思うが、依存症リスク要因の40~60%が遺伝的なものと推定されているらしい。ただ依存症の発症は患者の責任ではないが、依存症からの回復は患者の責任と述べられており、これは良い言葉だと思う。

本筋とは少し外れるが、遺伝的なリスク調査は一卵性双生児と二卵性双生児の比較で行うというのは初めて知った。

第3章 もっと食べたい

快感回路は薬物や電極によって刺激することができるが、ものを食べるといった自然に快感を伴う行動によっても活性化する。コカインなどの脳内にドーパミンを溢れさせる薬物をラットに与えると、そのラットはあまり食べなくなり体重を減らすとの事。覚せい剤が「痩せる薬」と呼ばれる事があるのも嘘では無いようだ。

肥満には遺伝的な要素が多分にあり、肥満しやすいラットは食事時に得られるドーパミンの上昇が他のラットよりも小さく、そのために一定のドーパミンレベルを達成するためにより多くの量を食べようとするのではないかと考えられている。

この遺伝性の肥満モデルは人間にも適用でき、体重の軽重は80%遺伝的に決まるとされている。これは身長などの身体的特性の遺伝性と同様であり、心臓病や統合失調症などの遺伝性よりはるかに高い。

身長が遺伝的に決まるというのは感覚的に分かるが、体重が遺伝的に決まるというのがあまりしっくり来ない人も多いのでは無いだろうか。 依存症の箇所でも言われていたが、一般的に心の弱さが原因であると言われる事でも実は遺伝的要因が大きいというケースがあるようだ。逆にこれを知っておく事で、気持ち的にラクになる人は多いだろうという気がした。

また、我々は脂肪と糖が豊富なものを食べた時の方がドーパミンが多く放出されるようになっている。脂肪と糖は同時に取ると極端に依存性が高くなり、普通の餌で満腹になっているラットも甘い餌を与えれば更に食べるとの事。これは「デザートは別腹」という事象を説明している。ここはたぶん雑学ポイントですね。

第4章 性的な脳

一番面白い章だったが、ブログに書くような内容でも無い気がするのでざっくりと。

人間は性的に特殊な生物である。哺乳類の90%は乱婚であるが人間は単婚の傾向を持ち、オスも子育てに協力する。 人間の同性愛的行動・マスターベーションなどは他の動物に無い奇妙な点と考えられる傾向にあるが、これはボノボなどの多くの哺乳類でも確認されている。

人間の性を独特なものにしているのは、むしろ最も因習的で社会的に是認されている交尾行動のほうなのである。

繁殖行動は身体的活動だがその心理的側面、すなわち恋愛について考えるとどうだろうか。恋愛に見られる精神的・生理的側面の表現は強烈な快感・恋人に対する判断の歪みなどで、文化によらず似通っている。

強烈な快感はドーパミン作動性の快感回路の活動に対応しており、これはコカインやヘロインへの反応に似ている。恋人に対する判断の歪みは前頭前皮質の活動低下に対応しており、これは強迫性障害と似ている。

恋愛と性的興奮は脳の活動という観点で見ると、両者とも快感回路の活性化を伴うという類似点を持つ。しかし、前者は判断中枢の低下を伴うが後者は伴わないという点で決定的に異なる。

セックスにはオーガズムの快感の他に終わった後に持続する暖かい余韻があるが、これは脳下垂体から分泌するオキシトニンというホルモンが影響している。 オキシトニンは社会的絆全般にも関係しており、オキシトニンの鼻スプレーをした被験者はプラセボのスプレーをした被験者よりも初めて会った相手を信頼しやすいことが分かっている。これらの結果から、オキシトニンは境界性パーソナリティ障害などの社会的認知に障害のある人の治療方法としても有望視されている。

「人を信頼しやすくなる」といった抽象的な事象が、ホルモンによってある程度説明できてしまうことにまず驚いた。ホルモンバランスが崩れると感情が制御できなくなる、というのは聞いた事があったが、やはり感覚的には受け入れられていなかったようだ。

第5章 ギャンブル依存症

一番身近な話だったので、そういう意味では一番楽しく読めた章。

ギャンブルへの嗜好は初期成功体験で身につくという説が一般的だが、ギャンブル好きな人の多くは初期成功体験など持っておらず、これはおそらく不完全である。 最近サルやラットの実験から提案されている別のモデルは、脳はもともとある種の不確実性に快感を見出すようにできているというものだ。

青い光を表示し、2秒後にシロップが五分五分の確率で出るような実験をサルに対して行った際には、青い光が点灯してから消えるまでの所謂待ち時間の間にドーパミンニューロンの発火レベルが徐々に高まっていく様子が確認された。

ギャンブルにまつわる非合理的な考え方の一つとして、「ニアミス」「直接介入効果」に関するものがある。

ニアミスは一つの負けではなく惜しかった勝負として認識され、ギャンブルを続けさせる要因になる。そしてランダムな事象であっても、賭ける人自身が個人的な関わりを持つ方が賭ける金額が多く、そして長くギャンブルを続けるようになるという事が研究によって知られている。 自分で何かを操作した時のニアミスは、満足度は比較的低いがゲームを続けされる力は強いという点は興味深い。

ビデオゲームには自然的な報酬性など一切ないが、プレイした際には快感回路がある程度活性化するビデオゲームは極めて効果的な報酬スケジュールを持っている可能性が高い。ちょうどタバコと同じで、快感自体は短いが、立ち上がりが早く何度も繰り返されるという形だ。

ギャンブルへの嗜好は初期成功体験によって身につく、という説は僕も支持していたし、おそらくかなりの人が支持していたのではないだろうか。

第6章 悪徳ばかりが快感ではない

慈善・社会的評価・隣人との比較・情報そのものなど、抽象的観念でさえも快感に変えられるという点が説明されていた。 面白いのは人間だけでなくサルでも抽象概念から快感を得られるという話。

第7章 快感の未来

カーツワイルは脳ナノボットの導入が2020年代、脳の内容や能力のアップロードが2030年代という時間的予想をしている。

この予想は、私たちの生物学・神経生物学についての理解がテクノロジーに導かれて指数関数的に深まるという仮説に基づいて成り立っているのだが、これに筆者は疑問を呈している。

筆者曰く、確かに人類は何人かのヒトゲノムの塩基配列を解析したし、その作業スピードやコストは指数関数的に改善してきているが、これらは有用であるものの遺伝子についての理解を指数関数的に深めるものではなく、ヒトゲノムの塩基配列が解析された時にそれを見て人間が個性的になる理由や受精卵が赤ん坊になる過程を急に理解できたような人はいなかったとのこと。

丁度少し前にニュースで似たような内容(2030年代に脳が無機物に移植可能になる)を聞いた時に「マジか」と思ったのだが、人の理解自体がテクノロジーに導かれて指数関数的に深まるという前提だと聞いて納得した。

例えば20年前と比べてプロセッサーやメモリの性能は指数関数的に向上したが、それによって例えば配管の亀裂進展への理解が指数関数的に進んだかと言われればそんな事は無いと思う。仮にヒトゲノムの解析が進んだとして、それが即ちすぐに脳の内容や能力のアップロードに繋がるのかというとそんな事も無さそうな気がしてしまう。

遠い未来の快感を思い描こうとするとき、いちばん想像しにくいのは未来のテクノロジーではなく、テクノロジーを取り巻く社会的、法的、経済的システムだ。

上で引用した言葉はまさにその通りだなあと思う。未来を舞台とした作品を読む時は、そこで描かれるテクノロジーではなく社会的システムにワクワクする。未来では無いが最近だと「BEASTARS」とか面白いなあと思う。

長くなりましたが、面白い本なのでオススメです。今は仕事の時間を自由に決められるので、インプットの時間を定期的に取ろうと思います。 なお、この本は下記ブログで紹介されていたので手に取りました。いつもありがとうございます。

migi.hatenablog.com

iOS/Androidの同時開発を高速化する設計手法について

明けましておめでとうございます。今年もよろしくお願い致します。

弊社では現在abceed analyticsというアプリを開発していますが、iOS/Androidのアプリを両方作る際の開発工数を削減したいというのは人類共通の課題かと思います。

そこで今回は、弊社がアプリ開発を高速化するために採用している手法・技術スタックについて簡単に書きます。 なお、React Native / Xamarin / Cordova などのクロスプラットフォームフレームワークについては触れません。あくまでネイティブで開発する際の手法ですのでご了承ください。

前提となる考え方

弊社では、iOS/Androidで新機能を同時にリリースすることは行なっておりません。多くの場合先行してiOS版で新機能をリリースし、遅れてAndroid版をリリースしています。 同時リリースを行なっていない理由は主に2つあります。

1つ目の理由は、新機能はリリース後にユーザーの反応を分析して変更が加えられる可能性が高いからです。 ソフトウェア企業が陥りがちな問題として「5%問題」というものがあります。これは、サービスを多機能化しすぎて何がサービスの核なのかを見失ってしまう、というものです。

gigazine.net

アプリに新機能を追加する際は、実装する前にその機能がサービスの核を損なわないかを十分考えるようにしていますが、実際にリリースしてみないと分からない事もあります。時にはリリース後に新機能を撤回することもありますので、まず片方のOSでリリースし、価値が高いことが分かったらもう片方のOSでもリリースするという方法が良いと感じています。※絶対に必要な機能である事が明確な場合や、開発リソースが潤沢な場合は別です

2つ目の理由は、UseCase層から先のコードはかなりの部分をiOS/Android間で使いまわせるからです。 後述しますが、弊社ではiOS/Android共にClean Architectureを使った設計を行っています。この場合、ビジネスロジックがViewや外部インフラに依存しなくなるので、コピペ + 一手間ぐらいの感覚で使いまわせるようになります。

ほぼ同じ内容のコードを2人の人間が別々に作るというのは効率が悪く、1人が作った後に使い回す方が開発工数・バグの出にくさの両面で優れていると感じています。ここに関しては考え方が分かれるかと思いますので、是非ご意見ください。

言語

iOSではSwiftを、AndroidではKotlinを使用しています。 Kotlinを使うべきかどうかに関しては諸説あるかとは思いますが、コードをiOS/Android間で使い回す際のやりやすさを重視しました。特にOptionalが言語レベルでサポートされている事が非常に大きいです。

Swiftでよく使うif letNil Coalescing OperatorもKotlinなら簡単に移植する事ができます。optional chainingに至っては全く一緒の構文です。

//Swift
//if let
if let book = bookOptional {
    print(book.name)
}
//Nil Coalescing Operator and optional chaining
print(bookOptional?.name ?? "book is null")
//Kotlin
//if let
bookOptional?.let {
    print(it.name)
}
//Nil Coalescing Operator and optional chaining
print(bookOptional?.name ?: "book is null")

あとはimmutableの宣言をvalletのどちらかに揃えて欲しいと切に願っています。

設計手法

個人のブログでも何回か触れていますが、設計手法はiOS/Android共にClean Architectureを使っています。

kakakazuma.hatenablog.com

qiita.com

詳しい内容についてはここでは触れませんが、ロジックを層ごとに分け、層と層の間の依存関係を無くす事でコード全体の見通しが良くなります。

パッケージ構成は超ざっくりだとこんな感じです。domainパッケージに関してはiOS/Android間でほぼほぼ使いまわせます。通常のClean Architectureを少し簡略化しているのでご注意ください。

├ data
│ ├ network
│ └ repository
├ di
├ domain
│ ├ value
│ ├ entity
│ ├ model
│ └ usecase
├ presentation
├ service
├ utils

UseCase層クラスの切り方は、エンティティでざっくり切るのとユースケース別に細かく切るのと2つあると思いますが、後者の方が見通しが良くなって移植はしやすい気がしています。ただクラス数が増えすぎるのでここは好みかもしれません。

class UserCRUDUseCase @Inject constructor(val rep: UserRepository) {
    fun register() {
        //some process
    }
    
    fun update() {
        //some process
    }
    
    fun delete() {
        //some process
    }
}
class RegisterUserUseCase @Inject constructor(val rep: UserRepository) {
    fun execute() {
        //some process
    }
}

設計手法は色々あるので好きなものを使えば良いと思いますが、迷っているならGoogleが公開しているAndroid Architecture Blueprintsを参考にするのがオススメです。

github.com

ライブラリ

iOS/Androidで使っているライブラリについてです。できるだけOS間で使用感が変わらないようなものを選んでいます。

API

iOSではAPIKit + ObjectMapperを、Androidではretrofit2 + Moshiを使っています。

APIのアクセス情報はdata/networkに格納します。APIのレスポンスに関してはdomain/entityに格納していますが、data層に格納して変換をdomain層で噛ませた方が良いと思う時もあります。

iOS

class UserInfoRequest: BaseAPIRequestType {
    
    var path: String {
        return "user/info"
    }

    var method: HTTPMethod {
        return HTTPMethod.get
    }
    
    //Response
    typealias Response = UserInfoResponse
    
    var id_user:String
    init(id_user:String) {
        self.id_user = id_user
    }
    
    //Request Parameters
    func toDict() -> Dictionary<String, Any> {
        var dict = Dictionary<String, Any>()
        dict["id_user"] = id_user
        return dict
    }   
}

class UserInfoResponse: Mappable {
    var id_user:String?
    var name_user:String?
    
    required init?(map: Map) {
    }
    
    func mapping(map: Map) {
        id_user <- map["id_user"]
        name_user <- map["name_user"]
    }
}

Android

interface UserApi {

    @GET("user/info")
    fun info(@Query("id_user") id_user:String): Call<UserInfoResponse>

}

data class UserInfoResponse(val id_user:String, val name_user:String)

内部DB

内部DBはiOS/Android共にRealmを使っています。シンプルで使いやすいですが、スレッドをまたぐ時に注意が必要です。 AndroidではUseCase層を別スレッドで実行する事が多いので、都度View用のモデルに変換しています。ここに関して良い方法あれば是非教えてください。

DI

AndroidではDagger2を導入しています。AndroidはContextが必要な箇所が多いのですが、Contextを引き回しているとコードの見通しが悪くなるのでDIを導入する利点が大きい気がしています。

DIを使うと、例えばSharedPreferenceが以下のようにRepository層に渡せるので、個人的に書きやすいです。

@Module
class AppModule {

    @Provides
    @Singleton
    fun provideContext(application: Application): Context {
        return application
    }

    @Provides
    @Singleton
    @Named("default")
    fun provideDefaultSharedPreference(context: Context): SharedPreferences {
        return PreferenceManager.getDefaultSharedPreferences(context)
    }
}

@Module
internal class RepositoryModule {

    @Provides
    @Singleton
    fun provideUserRepository(@Named("default") ref: SharedPreferences): UserRepository {
        return UserRepository(ref)
    }
}

class UserRepository @Inject constructor(val ref: SharedPreferences) : UserDataSource {
}

iOSに関しては、良いライブラリを見つけられていないこともあり手動でのDIとなってしまっています。良い方法ありましたら誰か教えてください。

まとめ

弊社では、

  • まず片方のOSでリリースし、価値が高いことが分かったらもう片方のOSでもリリースする

  • Kotlinを採用し、Swiftからの移植コストを下げる

  • Clean Architectureを採用してビジネスロジックを簡単にiOS/Android間で使い回せるようにする

  • AndroidでDagger2を採用し、Repository層の設計がiOS/Androidで同じになるようにする

といった方法で開発の高速化を図っています。 まだまだ拙いところも多くありますので、もし本記事にご意見などありましたら教えていただければ幸いです!

なお、弊社では現在エンジニアを募集中です。この記事を読んでもし興味がおありでしたら是非オフィスに遊びに来て下さい! アプリエンジニア以外にもWebエンジニア、データ分析屋さんも歓迎しています。

www.wantedly.com

BDD(振る舞い駆動開発)に則った自動テストでiOSアプリの開発速度を高める

この記事はiOS2 Advent Calendar 2017の8日目の記事です。

私事で恐縮ですが数ヶ月前に株式会社Globeeという会社のCTOに就任しまして、今はabceed analyticsという教育系アプリを開発しています。前職ではHadoop系を活用したログ収集基盤やログ解析基盤を担当していたので分野的には割と大きく変わりました。

さて、弊社のような小規模なスタートアップでは開発速度が重視されるため、自動テストがどうしても疎かになりがちです。

しかし個人的には小規模なスタートアップであっても、いけると思ったプロダクトならテストコードは書くべきだと考えています。理由はシンプルで、テストコードを書いた方が長期的に見て開発速度が上がるからです。

というわけで今回は弊社開発のアプリに自動テストを導入した時の考え方について話します。「うちはこうしている」などのアドバイス・ツッコミがありましたら是非コメントで教えてください!

テストコードを書く事で開発速度が上がる理由

私はテストコードを書く事で開発速度が上がる理由は大きく分けて以下の3つだと思っています。

  • コードを改修した時にバグが起きにくくなるので、バグの原因を調査する時間が少なくなり開発速度が上がる

  • テストコードがあることで新規メンバーが既存コードの仕様を理解しやすくなるので、チーム開発の速度が上がる

  • テスト可能なコードを意識する事でプロジェクト全体のコード品質が上がるので、長期的に見た時に開発速度が上がる

この辺りについては先日以下のような記事も上がっていましたので、既に認識済みの方も多いかと思います。

qiita.com

フロント開発における自動テスト

フロント開発はUI部分に頻繁に改修が入るため、バックエンド開発に比べてテストコードが形骸化しやすいです。ですので、個人的な意見ですがフロント開発ではテストカバレッジをそこまで追い求める必要は無いと思っています。

その辺を踏まえて、弊社では現在BDD(振る舞い駆動開発)の思想を採用しています。

BDD(振る舞い駆動開発)とは

BDDはTDDの亜種のようなもので、概要は以下です。

  • テストを「振る舞い」(機能的な外部仕様)の記述に特化させる、つまりユーザー目線でのテストとなる。

  • テストを実行可能なドキュメントとして扱い、テストの可読性を重視する。また、テストはユースケースの粒度で書かれる。つまり、テストコード=詳細設計書のような扱いとなる。

  • 振る舞いをテストするのであってコードをテストするのではないので、カバレッジはそこまで重要視しない。

詳しく知りたい人は以下のサイトも参考にしてください。

www.atmarkit.co.jp

特に重要だと思っているのが、テストを実行可能なドキュメントとして扱うという部分です。新規メンバーでもテストコードを読めば大体何をしているアプリなのか分かる、というのが理想かと思います。

SwiftではQuickがBDDテストフレームワークの代表格なので今回はそちらを採用しました。では実際にQuickを使った自動テストがどのようになるのか見てみましょう。

テストの前に

最初にも言いましたが、自動テストを導入するならコードがテスト可能な設計になっている必要があります。 超ざっくり言ってしまうと、

  • ネストが深すぎるコード
  • 長すぎるメソッド
  • でかすぎるViewController
  • 状態を持つシングルトン

があると自動テストが書きづらいです。

この辺はプロジェクトで使う設計手法を固めてしまえばある程度大丈夫かなと思います。 私はClean Architectureが一番しっくりきたのでそれを使っています。

Clean Architectureについて詳細は

qiita.com

tech.recruit-mp.co.jp

などをご参照下さい。

Quickを使ったシンプルな自動テストの例

前置きが長くなってしまいましたが、Quickを使って自動テストを書いてみます。

例えば特定の問題集がお気に入り登録されているかどうかを判定する以下のようなユースケースクラスがあったとします。(簡易化のためRepository層を切らずにRealmにアクセスしています)

class IsBookFavoritedUseCase {
    
    func execute(_ id:String) -> Bool {
        let realm = try! Realm()
        let predicate = NSPredicate(format: "id_book == %@",id)
        if let _ = realm.objects(MyBook.self).filter(predicate).first {
            return true
        } else {
            return false
        }
    }
        
}

このクラスに対してテストを書くと、以下のようになります。

class IsBookFavoriteUseCaseSpec : QuickSpec {

    override func spec() {
        let isBookFavoritedUseCase = IsBookFavoritedUseCase()

        describe("特定の問題集がお気に入り登録されているかどうかを判定できる") {
            let favoritedId = "book_favorited"
            let unfavoritedId = "book_unfavorited"

            beforeEach {
                //テストデータの準備
            }

            it("指定されたIDの問題集がお気に入り登録されていた場合はtrueを返す") {
                let isFavorited = isBookFavoritedUseCase.execute(favoritedId)
                expect(isFavorited).to(equal(true))
            }

            it("指定されたIDの問題集がお気に入り登録されていない場合はfalseを返す") {
                let isFavorited = isBookFavoritedUseCase.execute(unfavoritedId)
                expect(isFavorited).to(equal(false))
            }
        }

    }
}

中々可読性が高く、テストコードを読むだけでクラスの仕様が分かると思うのですがどうでしょうか?(テスト名に日本語を使うのが嫌いな人もいるかと思いますが)

ポイントはテスト名にメソッド名ではなく要求される振る舞いを記述しているという事です。これはBDDではコードをテストしているのではなく振る舞いをテストしているからです。

依存性を持つクラスの自動テスト

さて、次は現在地の緯度経度を元にユーザーが日本にいるかどうかを判定する以下のようなクラスを考えてみます。

class IsUserInJapanUseCase {
    
    func execute() -> Bool {
        let location = LocationGetter.shared.getUserLocation()
        return isInJapan(location)
    }
    
    private fun isInJapan(location:CLLocation?) -> Bool {
        //do some check process
    }
}

このクラスは位置情報取得処理がLocationGetter(架空のクラスです)に依存しているため、このままでは単体テストを行うことができません

このような状態を実装に依存していると呼びます。実装への依存を避けるためのデザインパターンDI(依存性注入)です。

DIで実装依存を避ける

サービスクラスをインタフェース経由で使うようにし、実体を外部から渡せるようにするのがDIです。 詳しく知りたい方は以下の記事が良いかもしれません。

qiita.com

qiita.com

cocoacasts.com

今回の例だと、LocationGetterは現在地を取得し返すという振る舞いを持つクラスです。なので、まずはprotocolとしてその振る舞いを定義します。

protocol LocationAccessable {    
    func getUserLocation() -> CLLocation?
}

次にLocationGetterLocationAccessableを継承するようにし、IsUserInJapanUseCaseLocationAccessable経由で位置情報の取得を行うようにします。

class LocationGetter: LocationAccessable {

    func getUserLocation() -> CLLocation? {
        //CLLocationManagerを用いた実際の位置情報取得処理を実装
    }
    
}
class IsUserInJapanUseCase {
    
    var locationAccessor:LocationAccessable

    func execute() -> Bool {
        let location = locationAccessor.getUserLocation()
        return isInJapan(location)
    }
    
    private fun isInJapan(location:CLLocation?) -> Bool {
        //do some check process
    }
}

以上のようにする事でIsUserInJapanUseCase位置情報取得の実装クラスを外部から渡せる実装に依存しないクラスとなり、単体でテスト可能となりました。では実際にテストを行ってみましょう。

テストのためにLocationAccessableプロトコルを継承したモッククラスを作成します。

class FakeLocationGetter: LocationAccessable {

    var fakeLocation = CLLocation(latitude: 35.666401, longitude: 139.754207)
    
    func getUserLocation() -> CLLocation? {
        return fakeLocation
    }
    
}

このモッククラスは、fakeLocationの値を変える事で簡単に返却値を切り替える事ができます。このクラスをテスト実行時にIsUserInJapanUseCaseに以下のように注入します。

class IsUserInJapanUseCaseSpec : QuickSpec {

    override func spec() {
        let isUserInJapanUseCase = IsUserInJapanUseCase()
        let fakeAccessor = FakeLocationGetter()

        describe("現在地の緯度経度を元にユーザーが日本にいるかどうかを判定できる") {

            beforeEach {
                isUserInJapanUseCase.locationAccessor = fakeAccessor
            }

            it("日本の緯度経度にはtrueを返す") {
                fakeAccessor.fakeLocation = CLLocation(latitude: 35.666401, longitude: 139.754207)
                let isInJapan = isUserInJapanUseCase.execute()
                expect(isInJapan).to(equal(true))
            }

            it("日本以外の緯度経度にはfalseを返す") {
                fakeAccessor.fakeLocation = CLLocation(latitude: 46.532219, longitude: 116.460937)
                let isInJapan = isUserInJapanUseCase.execute()
                expect(isInJapan).to(equal(false))
            }
        }

    }
}

このようにDIとモッククラスを活用することで、依存性を持つクラスでもテストを行うことができます。

今回は位置情報取得部分を例としましたが、API通信・ポップアップ表示・ローカル通知などもこの方法でテストを行うことができます。

詳しくは以下が参考になるかと思います。

academy.realm.io

まとめ

自動テストの導入には若干のコストがかかりますが、長期的に見れば絶対にペイすると思います。小規模なスタートアップであっても、このプロダクトはいけると思ったら積極的に自動テストを導入しましょう!

なお、弊社では現在エンジニアを募集中です。こんな長い記事を最後まで読んでくださった方は多分技術が好きな方だと思います。もし弊社に興味ありましたら是非一度お話しさせてください!

www.wantedly.com