サナギわさわさ.json

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

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

UdacityのDeep Learning Nanodegree Foundationを修了しました

UdacityのDeep Learning Nanodegree Foundationを修了しました。受講期間は約4ヶ月でした。

Deep Learning Nanodegree Foundation | Udacity

f:id:kakakazuma:20170517222932p:plain

主な学習内容

  • numpyでの多層パーセプロトン実装(これ以降は全てTensorFlow)
  • 畳み込みニューラルネットによる画像分類
  • RNNによるTV番組の会話文字列生成
  • LSTMによる英語-フランス語翻訳
  • DCGANによるMNIST, CelebAの画像生成

課題の内容は GitHub - udacity/deep-learningで確認できます。

受講した感想

4ヶ月という短期間でDeep Learningの分野を幅広く学べたので、初学者の入りとしては中々良かったかなと思います。

レビュアーがついてくれるので、行き詰まるということがあまり無かったのが好印象です。例えば「この場合weightの初期化を一様分布では無く正規分布で取った方が良い」とか、「Labelをスムージングした方が良い」とか初学者が気づきにくいようなところを指摘してくれるのは有り難かったです。学習の助けになりそうな記事もたくさん紹介してくれました。(全て英語記事ですが)

紹介してくれた英語記事などは個人的な備忘録としてこちらにまとめていますので、もし興味あれば。

数式少なめの機械学習系記事まとめ

ただ4ヶ月で幅広くやるという都合上、1つの分野の掘り下げが浅くなりがちだったので、興味を持った分野に関してはもう少し自分で学習しようと思います。

注意点

僕が登録した時は受講費用は$499でしたが、まあ安くは無い金額だと思います。なので一応注意点を書いておきます。

あくまで初学者向け

ターゲットは初学者向けなので、既にディープラーニングをある程度知っている方にはやや物足りない内容かもしれません。 GitHub - udacity/deep-learningを見て自分のレベルに見合うものかどうかを確認すると良いかと思います。

そこそこの学習コストがかかる

大体週8時間ぐらいは学習する必要があるかと思います。英語が苦手だともう少しかかるかもしれません。 また、課題ごとにタイムリミットがありますので注意です。

多少のプログラミング知識が必要

講義は全てPythonで行われますので、プログラミング自体が初めてだと厳しいかと思います。また、数式はあまり出てきませんが行列計算と微分積分の基礎ぐらいは必要な気がします。

Programming knowledge needed: Basic to intermediate Python, experience with Numpy. Anaconda and Jupyter Notebooks.

まとめ

少し高いですが、Deep Learningを初めて学習するなら悪くない選択肢だと思います!

Udacityのディープラーニングのナノ学位基礎コースを受講します

ネットサーフィンしてたらUdacityがディープラーニングのナノ学位基礎コースを立ち上げ、399ドルで志願者全員入学の記事を見つけたので、受講してみることにしました。

ディープラーニングの基礎の基礎ぐらいは分かっていると思うのですが、どうも具体的なアプリケーションに落とし込むイメージが湧いていないのでそれの足がかりになればなと思います。あと英語勉強も一緒にやりたい。

時間あれば学んだ内容をまとめて記事にできればなと思います。