Posted at
iOSDay 18

App ExtensionsのUnit testing

More than 3 years have passed since last update.

App Extensionsのテストコードを書きたいなと思って、いざやってみると少し苦労した話です。

2014/12/17 potatotips #12で発表した内容です。

発表資料はこちらです。

前置きとして簡単にApp Extensionsとユニットテストの説明を記載しましたが、既にご存知の方は飛ばしてください。


App Extensionsとは


iOS 8.0とOS X v10.10以降、App Extensionsにより、アプリケーションを超えて独自の機能と

コンテンツを拡張でき、ユーザが他のアプリケーションやシステムを利用している場合でも、それ

らを利用することができます。



提供されているExtension

Extension point
Typical app extension functionality

Today (iOS and OS X)
Get a quick update or perform a quick task in the Today view of Notification Center (A Today extension is called a widget)

Share (iOS and OS X)
Post to a sharing website or share content with others

Action (iOS and OS X; UI and non-UI variants)
Manipulate or view content originating in a host app

Photo Editing (iOS)
Edit a photo or video within the Photos app

Finder Sync (OS X)
Present information about file sync state directly in Finder.

Document Provider (iOS; UI and non-UI variants)
Provide access to and manage a repository of files.

Custom Keyboard (iOS)
Replace the iOS system keyboard with a custom keyboard for use in all apps

Watch App (iOS)
Provide an app, a glance, or a notification UI for Apple Watch, as described in Apple Watch Programming Guide.


基本的な構造

各Extensionは必ず紐づくiOSアプリが必要です。

App Extensionsの開発は、ターゲットを新たにアプリケーションに追加して行います。

Extensionを持っているアプリのことをContaining appと言います。

今回は主にユニットテストの話をするので、詳しい内容は公式のドキュメントを読んでください。

App Extension Programming Guide

App Extensionsプログラミングガイド


Xcodeのユニットテスト


ユニットテストは、コードが設計仕様に則しているか検証し、さらに、修正を施す際にも仕様に反し

ないようにするための手段です。堅牢でセキュアなアプリケーションを記述するためにも有用です。

ユニットテストの重要な構成要素として、テスト可能な最小単位(ユニット)でコードをテストす

る、「テストケース」という概念があります。



ユニットテストターゲット

最近のXcodeでは新規プロジェクト作成時にデフォルトでテストターゲットが追加されています。

ProjectNameTestsという名前になっているはずです。

もしテストターゲットが無い場合は、新しくターゲットを追加から、Cocoa Touch Testing Bundleを選択して作成しましょう。


テストケースメソッド


テストケースメソッドはテストしようとするコード(これをユニットという)を呼び出して、その呼び出し

により期待どおりの結果が得られるか(たとえば、期待した戻り値が戻るかどうか、例外が発生する

かなど)をテストしてレポートします。


テストケースメソッドはtest...という名前のメソッドで、パラメータは無く、戻り値の方はvoidです。

各テストケースメソッドの前後に呼び出されるsetUptearDownをテストクラスに追加出来ます。

テストケースメソッドでは、期待された条件を確認したりその結果をレポートするために、以下のマクロを使用出来ます。


  • XCTFail(format...)

  • XCTAssertEqualObjects(expression1, expression2, format...)

  • XCTAssertNotEqualObjects(expression1, expression2, format...)

  • XCTAssertEqual(expression1, expression2, format...)

  • XCTAssertNotEqual(expression1, expression2, format...)

  • XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...)

  • XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...)

  • XCTAssertGreaterThan(expression1, expression2, format...)

  • XCTAssertGreaterThanOrEqual(expression1, expression2, format...)

  • XCTAssertLessThan(expression1, expression2, format...)

  • XCTAssertLessThanOrEqual(expression1, expression2, format...)

  • XCTAssertNil(expression, format...)

  • XCTAssertNotNil(expression, format...)

  • XCTAssertTrue(expression, format...)

  • XCTAssert(expression, format...)

  • XCTAssertFalse(expression, format...)

  • XCTAssertThrows(expression, format...)

  • XCTAssertThrowsSpecific(expression, exception_class, format...)

  • XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, format...)

  • XCTAssertNoThrow(expression, format...)

  • XCTAssertNoThrowSpecific(expression, exception_class, format...)

  • XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, format...)


テスト対象のターゲット設定

テストターゲットのGeneralからHost Applicationを選択出来ます。

Screen Shot 2014-12-17 at 17.26.14.png


App Extensionsのユニットテスト

さて、ようやく本題です。

実は、先ほどのHost Applicationを選ぶところに肝心のExtension用のターゲットが出てきません。

コマンドラインから実行出来るxcodebuildにもしかしたら新しいオプションが用意されているかと思いましたが見当たりません。

色々な方法を試してみましたがお手上げなのでDeveloper Forumsで質問してみました。

以下が回答内容です。


  • Compile code from your extension into your test bundle

  • Factor the code to be tested into a shared library, and link you test bundle to that library

1番重要なことは、Xcode6ではApp Extensionsのユニットテストがまだサポートされていないということが分かりました。。。

ただ、回答内容に書いてある通りで方法はあります。

コードをフレームワーク化して、そのコードをテストメソッドの中で呼んでテストしろとのことです。


Embedded Framework

Embedded Frameworkを作成すると、App ExtensionsとContaining Appの間でコードを共有出来ます。

詳細な手順に関しては、クラスメソッドさんが分かりやすくまとめてくださっています。

[iOS8] App Extension #2 - Embedded Frameworkを利用して共有コードをFramework化する

上記の手順に従って進めて必要なクラスをFrameworkに定義します。

今回は説明用に、PotatotipsFrameworkというフレームワークにPotatotipsUser.hPotatotipsUser.mを作成しました。

ExtensionとContaining App間でAppGroupを使用したNSUserDefaultにユーザー名の取得・保存が出来るクラスです。

Screen Shot 2014-12-18 at 15.59.17.png

PotatotipsUser.h

#import <Foundation/Foundation.h>

@interface PotatotipsUser : NSObject

+ (NSString *)name;
+ (void)setName:(NSString *)name;

@end

PotatotipsUser.m

#import "PotatotipsUser.h"

NSString * const AppGroupID = @"group.potatotips";
NSString * const NameKey = @"name";

@implementation PotatotipsUser

+ (NSString *)name
{
return [[self sharedUserDefaults] objectForKey:NameKey];
}

+ (void)setName:(NSString *)name
{
NSUserDefaults *userDefaults = [self sharedUserDefaults];
[userDefaults setObject:name forKey:NameKey];
[userDefaults synchronize];
}

+ (NSUserDefaults *)sharedUserDefaults
{
return [[NSUserDefaults alloc] initWithSuiteName:AppGroupID];
}

@end


Embedded Frameworkをユニットテストで利用する

利用方法は簡単で、@importと記述してimportするだけです。

今回使用したテストクラスは下記の通りです。

PotatotipsTests.m

#import <UIKit/UIKit.h>

#import <XCTest/XCTest.h>

@import PotatotipsFramework;

@interface PotatotipsTests : XCTestCase

@end

@implementation PotatotipsTests

- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}

- (void)testExample {
// This is an example of a functional test case.
XCTAssert(YES, @"Pass");

[PotatotipsUser setName:@"hoge"];
XCTAssertEqualObjects(@"hoge", [PotatotipsUser name]);
}

- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}

実行結果は下の画像の通りでテストが正常に通っています。

Screen Shot 2014-12-18 at 19.24.56.png


テスト自動化

xcodebuildコマンドをCIに入れればテストが簡略になります。

特に特別なことをする必要は無く、testコマンドを実行するだけで十分です。

xcodebuild test –workspace Potatotips.xcworkspace –scheme Potatotips -destination 'platform=iOS Simulator,name=iPhone 6’


まとめ


  • Embedded Frameworkを利用することで、コード量も減りテスト可能になる

  • App Extensionsのようにデバッグしている最中にあまり見ない画面こそテストコードを書いて運用コスト削減

  • 完全に趣味の領域でUIテストもしたかったが、Extensionで使用するUIにはApple標準のUIが多く含んでいるのでさすがに大丈夫だと信じている

今回作ったクラスはテストするまでも無い簡単な機能ですが、実際に使うであろう機能はログイン処理や非同期アップロードや画像加工などがあり、ロジックテストが必要なものがあると思います。

個人的にテストのポイントだと感じているのが、普段デバッグしない機能や画面のテストコードを書くということです。 なぜなら、自分が担当した作業が思わぬ箇所で悪影響を及ぼして不具合の原因になることが少なからずあると思っているからです。 主にUIに当てはまるケースですが、こういった問題が起こるのが例えば新規登録画面の処理だったとして、普段この辺りはあまりデバッグせず、不具合が起きててもエンジニアが気付かずにユーザーの問い合わせで初めて気づくということを避けたいのでテストコードについて調べてみました。 たまにはテストコードを書くのも良いかもしれませんね。