CocoaPodsでGHUnit + OCMockなiOS開発環境を構築する

なにこれ

新しいプロジェクト作るたびにいつも構築手順を忘れてるのでメモ。 おまけでコンソールからのテスト実行(Jenkinsとの統合が目的)のやり方も書いた。

対象者

  • Objective-CによるiOS開発者
  • CocoaPodsを知っている・使いたい
  • GHUnitを知っている・使いたい
  • (JenkinsでCIしたい)←必須じゃない

検証環境

2012年12月9日時点で最新のもの。

手順

1. Xcodeでプロジェクト作成

Unit TestはGHUnitを使うので不要。 このドキュメントでは MyProject とする。

2. 新しいターゲット作成

プロジェクト設定を開いて、テスト実行用のターゲットを作成する。 ターゲットの種類はiOSのEmpty Application、名前は Tests にしておく。 名前は変更してもいいけど、以下の説明で Tests と出てきたら自分で読み替えること。

自動生成されるファイルのうち、次のものだけ残して全部消す(ファイルごと)

  • Tests-Info.plist
  • InfoPlist.strings
  • main.m
  • Tests-Prefix.pch

3. Podfile作成

MyProject.xcodeproj と同じ所に Podfile を作成する。

platform :ios, '5.0'

pod 'AFNetworking',  '~> 1.0.1'
pod 'CocoaLumberjack', '~> 1.6'

target :Tests, :exclusive => true do
  pod 'GHUnitIOS', '~> 0.5.5'
  pod 'OCMock', '~> 2.0.1'
end

一行目でiOS 5.0以上を指定しているが、実際には作成するプロジェクトに合わせて指定する。 AFNetworkingとCocoaLumberjackは実際に作ったPodfileから取ってきただけのただのサンプル。実際に必要な物を指定して欲しい。 このとき target :Tests を2.で作ったターゲット名にするのを忘れないこと。

終わったらコンソールからworkspaceを作成してもらう。

pod install

4. xcodeproj閉じてxcworkspace開く

両方同じxcodeprojを参照するのでわけわからないことになる前に xcodeproj を閉じる。

5. ターゲット TestsBuild Settings いじる

Other Linker Flags-ObjC -all_load を追加する。 俺の環境では -ObjC はすでに追加されていた。

6. main.m いじる

//
//  main.m
//  Tests
//
//  Created by <ユーザ名> on 12/8/12.
//  Copyright (c) 2012 <組織名>. All rights reserved.
//

#import <UIKit/UIKit.h>

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, @"GHUnitIOSAppDelegate");
    }
}

ここまでやってiPhone Simulatorで起動するとGHUnitのテスト実行画面が表示される。

7. ユニットテスト作ってみる

GHUnit

ここまでうまくいってるか確認するために一個テスト作ってみる。 Testsに一個Objective-Cのクラスを作る。 GHUnitのガイド にあるコードのままだとコンパイルが通らないのでちょいと手直し。

MyTest.h

#import <GHUnitIOS/GHUnit.h>

@interface MyTest : GHTestCase

@end

MyTest.m

#import "MyTest.h"

@implementation MyTest

- (void)testStrings {
    NSString *string1 = @"a string";
    GHTestLog(@"I can log to the GHUnit test console: %@", string1);

    // Assert string1 is not NULL, with no custom error description
    GHAssertNotNil(string1, nil);

    // Assert equal objects, add custom error description
    NSString *string2 = @"a string";
    GHAssertEqualObjects(string1, string2, @"A custom error message. string1 should be equal to: %@.", string2);
}

@end

修正前はGHAssertNotNULLを使ってたのでGHAssertNotNilに変えた。

もっかいiPhone Simulator起動してみてテストできることを確認する。

OCMockも確認する

MyMockTest.h

#import <GHUnitIOS/GHUnit.h>
#import <OCMock/OCMock.h>

@interface MyTest : GHTestCase

@end

MyMockTest.m

#import "MyMockTest.h"

@implementation MyMockTest

- (void)testMock {
    id mock = [OCMockObject mockForClass:[NSString class]];
    [[[mock stub] andReturnValue:OCMOCK_VALUE((NSUInteger){100})] length];

    GHAssertEquals(100U, [mock length], @"ocmock works");
}

@end

iPhone Simulator起動してみてテストできることを確認する。

8. コンソールからテスト実行してみる

Jenkins用なんで興味ない人はスルー。 基本は GHUnitのガイド 通り。

i. シェルスクリプトを取ってくる

xcodeproj, xcworkspaceがあるところで以下を実行。

wget https://raw.github.com/gabriel/gh-unit/master/Scripts/RunTests.sh
wget https://raw.github.com/gabriel/gh-unit/master/Scripts/RunIPhoneSecurityd.sh

ii. テスト実行時にスクリプトを実行するようにする

プロジェクト設定内のTestsBuild Phasesを開き、Add Build Phase -> Add Run Scriptを選択。 入力欄にsh RunTests.shと入れる。

iii. Makefile

GHUnitのドキュメントにはworkspaceのときの例がないのでそのままだと動かない。 適当に作ってみる。

Makefile

clean:
	-rm -rf build/*

test:
	GHUNIT_CLI=1 xcodebuild ONLY_ACTIVE_ARCH=NO -workspace <xcworkspaceファイルへのパス> -scheme Tests -configuration Debug -sdk iphonesimulator build

ワークスペースへのパスに書き換えるのを忘れないこと。 連続空白に見えるところはタブ文字にすること。

iv. (Base SDKが6.0のときのみ) main.mの修正

2012年12月9日現在、iOS SDKが6.0だと、コンソールから動かせない問題がある(GHUnitのIssue at GitHub)。 暫定措置だが一番下のgfxさんのコメント通りにmain.mを書き換えるとコンソールから動かせるようになる。

v. テストを実行する

make test

echo $?して0だったら、おめでとう。すべてのテストをパスしたぞ!

9. Jenkinsからテスト実行

まず前章で作ったRunTests.sh, RunIPhoneSecurityd.sh, Makefileをgit commitしておく。 公開前提のプロジェクトだと、git cloneしたユーザにpod installを実行させるのでxcworkspaceをソースコードリポジトリ管理しないと思う。

なのでその流儀に則り、Jenkinsでもpod installすることにする。

i. ジョブを作る

名前はMyProjectにしてみた。フリースタイル・プロジェクトを選択。

ii. 設定

ソースコード管理システムでソースの位置を指定する。 俺はローカルホストにJenkinsを立ててるので、GitのRepository URLがfile:///からはじまる。

ビルドに シェルの実行 を追加する。

WORKSPACE_FILE=MyProject.xcworkspace
if [ ! -d "$WORKSPACE_FILE" ]; then
  pod install
  open -R "$WORKSPACE_FILE"
  echo "We need to init the workspace file. Please try re-run test after you open workspace file."
  exit 1
fi
make clean && WRITE_JUNIT_XML=YES JUNIT_XML_DIR=tmp/test-results make test

最初のビルドではxcworkspaceの初期化が終わっていないため、 xcodebuild: error: The workspace 'MyProject' does not contain a scheme named 'Tests'. と失敗してしまう。

少し悩んだが、ユーザにXcodeでxcworkspaceを開かせることにした。 一度Xcodeで開いてしまえばもうこの作業はやらなくてOKだ。

open -R <ファイルパス> はFinderでファイルを表示するコマンド。

ビルド後の処理 から JUnitテスト結果の集計 を選び、tmp/test-results/*.xmlと指定する。

ビルドしてみると一回目は失敗すると思う。 xcworkspaceがFinderで表示されていると思うので、ダブルクリックして初期化してから再度ビルドすればテストが実行されるはずだ。

このへんはバッドノウハウなんで、もっといい方法あるって人は教えて欲しい。

サンプル

https://github.com/mtgto/CocoaPodsSample

ここまでの説明があってるかの確認のために作ってみた。

参考

全部英語のページ。