5.1さらうどん

@giginetの技術ブログ。ゲーム開発、iOS開発、その他いろいろ

SwiftでJSONのテストを良い感じにするJSONMatcher作った

この度、JSONMatcherというSwift向けのテストライブラリを開発しました。

github.com

これは何?

SwiftでJSONのオブジェクトや文字列を検査するマッチャーです。

JSONオブジェクトのテストがこんな感じで簡単に書けます。

import XCTest
import Nimble
import JSONMatcher

class ExampleTestCase: XCTestCase {
    func testComplexExample() {
        expect([
            "name" : "Snorlax",
            "no" : 143,
            "species" : "Sleeping",
            "type" : ["normal"],
            "stats" : [
                "hp" : 160,
                "attack" : 110,
                "defense" : 65,
                "special_attack" : 65,
                "special_defense" : 65,
                "speed" : 30
            ],
            "moves" : [
                ["name" : "Tackle", "type" : "normal", "level" : 1],
                ["name" : "Hyper Beam", "type" : "normal", "level" : NSNull()],
            ]
        ]).to(beJSONAs([
            "name" : "Snorlax",
            "no" : Type.Number, // value type matching
            "species" : try! NSRegularExpression(pattern: "[A-Z][a-z]+", options: []), // regular expression matching
            "type" : ["[a-z]+".regex], // shorthands for NSRegularExpression
            "stats" : [
                "hp" : 160,
                "attack" : 110,
                "defense" : 65,
                "special_attack" : 65,
                "special_defense" : 65,
                "speed" : 30
            ],
            "moves" : [
                ["name" : "Tackle", "type" : "[a-z]+".regex, "level" : Type.Number], // nested collection
                ["name" : "Hyper Beam", "type" : "normal", "level" : NSNull()],
            ]
        ]))
    }
}

JSONMatcherは、Swift製のマッチャーライブラリ、Nimbleの拡張として動作し、与えられたJSON文字列、オブジェクトを簡単に比較できます。

上記のように、複雑なJSONであっても直感的にマッチャーを書くことができます。

マッチャーとして

  • 与えられたオブジェクトや文字列がJSONであることを判定するbeJSON
  • 特定のオブジェクトを含むかを判定するbeJSONIncluding
  • 2つのオブジェクトが一致することを判定するbeJSONAs

の3種類を提供しています。

expect("{\"name\": \"Pikachu\"}").to(beJSON())
expect(["name" : "Pikachu", "no" : 25]).to(beJSONIncluding(["name" : "Pikachu"]))
expect(["name" : "Pikachu", "no" : 25]).to(beJSONAs(["name": "Pikachu", "no" : 25]))

また、上記の例にあるとおり

  • 正規表現NSRegularExpression
  • 型マッチングのようなもの

にも対応していて、かなり柔軟に比較することができます。

このライブラリは、rspec-json_matcherに大変影響を受けてます。

github.com

Swiftでライブラリを作る

今回のモチベーションの一つとして、Swiftのライブラリを最新のエコシステムを用いてしっかり作ってみたいというものがありました。

Swiftのライブラリ作成の知見は枯れておらず、有名OSSを眺めてみてもあまり統一されていなかったので、今回作成した知見をまとめてみました。

f:id:gigi-net:20160521182333p:plain

しっかりテストして、こういう風にバッヂを貼りまくると、ちゃんとしたライブラリっぽくなります。

ライブラリ作成にあたって、3月に行われたtry! Swiftの「Creating a Swift Library」というセッションが大変参考になってオススメです。

以下の書き起こしやリポジトリを参考にしてみてください。

try! Swift ライブラリの開発 #tryswiftconf Day2-8 - niwatakoのはてなブログ

github.com

ディレクトリ構造

まずSwiftのライブラリを作るにあたって、ディレクトリ構造からして悩むのですが、結論から言えば、最小構成で以下のような感じにすれば良さそうです。

実装はSources/LibraryName以下にソースコードTests/LibraryName以下にテストコードを配置するのがベストのようです。

├── Cartfile
├── Carthage
│   └── Checkouts
├── JSONMatcher.xcodeproj
├── Sources
│   └── JSONMatcher
│       ├── *.swift
│       └── Info.plist
└── Tests
       └── JSONMatcher
           ├── *.swift
           └── Info.plist

テスト

f:id:gigi-net:20160521185739p:plain

テストは必ず記述しましょう。XCTestをそのまま使っても良いですが、今回は前述のNimbleを用いて記述しています。

今回は使用していませんが、さらにRspecっぽく書きたい方はQuickを併せて使っても良いでしょう。

テストの実行には簡単なShellスクリプトを用意して、xcodebuild testを実行してます。

マルチプラットフォーム

Swift向けのライブラリは、iOSOSX、tvOS、watchOS、Linuxの5つの環境向けに作ることができます。

今回はiOSOSX、tvOS向けに対応させてみました。

watchOSではそもそもXCTestが使えないため、今回は対応できず、Linux対応は面倒そうなのでやっていません。

マルチプラットフォーム対応は簡単で、以下のようにプラットフォーム別にターゲットとスキーマを作成しましょう。

f:id:gigi-net:20160521191145p:plain

最初はiOS向けのみに実装し、テストがある程度揃ってきたら、テストを実行しつつ、マルチプラットフォーム化をするのが楽でした。

CI

CI as a Serviceとして、Travis CIやCircle CIが使えます。好きなものを使いましょう。

CIでは以下のような事を行っています。

  • 各プラットフォーム向けのテストの実行
  • カバレッジのレポート
  • swiftlintの実行
  • CocoaPodsの検証

また、先日、Travis CIでのPublicリポジトリ向けのキャッシュの提供が開始されました。Swiftのライブラリはビルド時間が長くなりがちなので、依存ライブラリのビルドに時間がかかる場合は利用すると良いでしょう。

カバレッジ

ついでにテストカバレッジを取得しましょう。Xcode7からはテストスキーマのチェックを入れるだけで、簡単にカバレッジを計測できるようになりました。

f:id:gigi-net:20160521191244p:plain

計測したカバレッジは、CIからcodecov.ioというサービスに送っています。codecovはSwift対応が楽で大変便利です。

f:id:gigi-net:20160521191005p:plain

Coverallsでの表示も可能ですが、公式でサポートされていないようでした。

パッケージ管理ツール

現在、Swiftのパッケージ管理ツールとして、CarthageCocoaPodsSwift Package Manager(SPM)の3種類がよく知られています。

対応するための作業はそれほど多くないので、とりあえず全てに対応させてます。

Carthage

基本的に上記の通り、プラットフォーム別にスキーマを作ってあげると、良い感じで対応できます。

今回のように依存関係があるライブラリの場合はCartfileに依存環境を記述しましょう。

Carthageの設計思想は、Xcode標準のスキーマを使ったシンプルなビルドシステムであり、Carthageに対応して作ると良い感じの構造になるので、意識して設計すると良さそう。

CocoaPods

pod spec createなどを実行すると、podspecのひな形ができるので良い感じで記述していきます。

また、CIでpod lib lintなどを回すようにしておくと、podspecの検証ができます。

完成したPodは公式のリポジトリに登録すると良いでしょう。

SPM

Carthage対応ができていればPackage.swiftというファイルを置くだけでなんとなく対応できます。

ほとんどユーザーはいないと思うのでちゃんと動くかどうかはあんまり確認してません。

静的解析

静的解析ツールとして、Realmが提供しているSwiftLintを使って、Travis CI上で回しています。

github.com

SwiftLintはデフォルトの設定だと厳しすぎるので、.swiftlint.ymlという設定ファイルで挙動をカスタマイズすることができます。(参考

どの警告を抑止すべきか、開発元のRealmでの利用例を踏襲しておけば問題ないと思ったので、主にここの設定を参考にしています。

まとめ

ざっくりと開発で培った知見をまとめてみました。参考になれば幸いです。

また、テストケースでJSONを扱う需要がある方は是非JSONMatcherをご利用ください。Pull Requestも歓迎です。

github.com