iOSのロジックテストをはじめる

iOSで初めてまじめにテスト書こうと思いまして・・!(←色々と問題)
とっても最初の、スタート地点より手前なところからまとめていこうと思います。

環境はXCode7です。

ロジックテストとUIテスト

XCodeで用意されてるテストは2種類あります。

  • ロジックテスト(XCode上では Unit Tests と記されている)
  • UIテスト

このうちのロジックテストについて取り上げます。
(UIテストは・・またいつの日かっ)

準備

プロジェクトを作成すると、ロジックテスト&UIテストが存在していて、テストを書ける状態が既に整っています。

これまで特に意識してませんでしたが、
Applicationプロジェクト作成時に、テストに関するチェックボックスがあります。

f:id:takopomm:20151009011451p:plain

  • Include Unit Tests
  • Include UI Tests

標準ではどちらもチェック済みになっているので、 何も意識せずともテストが実行できる環境が出来上がっているのでした。(ありがたや)

Libraryプロジェクトの場合

Libraryプロジェクト作成時には、上記のようなチェックボックスがありません。

f:id:takopomm:20151009011532p:plain

プロジェクト作成が終わった後に、自分でテストターゲットを追加します。

テストターゲットを追加する

Libraryプロジェクト作成直後の状態から、テストターゲットを追加してみます。

f:id:takopomm:20151009011821p:plain

1.メニューバーから File - New - Target を選択

2.iOS - Test - iOS Unit Testing Bundle を選択

f:id:takopomm:20151009012114p:plain

3.Product Name にテストターゲットの名前を入力して Finish

f:id:takopomm:20151009012139p:plain

テストターゲットの名前は

  • ロジックテスト : プロジェクト名Test
  • UIテスト : プロジェクト名UITest

とするのが標準的なようです。

f:id:takopomm:20151009012207p:plain

これで、テストを実行できるようになりました。
導入の敷居が低くてステキですね。

(もっと早くから取り組むべきだったんですけど・・・汗汗汗・・・

今日は、テストケースを先に書いてからメソッドの中身を実装する、という事をやってみました。なかなかおもしろかったです。状況に応じてまたやってみようっと。

iOSでオーディオをバックグラウンド再生するための設定

(特に目新しい内容はありませんが、ちょっと時が経ったら忘れていたのでメモです)

標準の状態では、アプリがバックグラウンドに行った時点で再生停止するようになっています。

AudioSession

AudioSessionの設定は、再生開始前に行います。

AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];

オーディオを扱うアプリにとって必須な基本設定です。
バックグラウンド再生の有無に関わらず行います。

info.plist

info.plistに、以下を追加します。

Required background modes
- App plays audio or streams audio/video using AirPlay

この設定で、バックグラウンド再生が可能になります。

MenuItemCompat.getActionViewがnull

凡ミス。

検索画面を実装していて、 MenuItemCompat.getActionView()がnullになってアレー

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {

        getMenuInflater().inflate(R.menu.menu_search, menu);

        MenuItem menuItem = menu.findItem(R.id.action_search);
        searchView = (SearchView)MenuItemCompat.getActionView(menuItem);
        ...

原因はxmlでした。

    <item android:id="@+id/action_search"
        android:title="Search"
        android:icon="@drawable/ic_search"
        android:orderInCategory="100"
        android:actionViewClass="android.support.v7.widget.SearchView"
        app:showAsAction="collapseActionView" />

actionViewClassは、android:ではなくてapp:でした。

        app:actionViewClass="android.support.v7.widget.SearchView"

MenuItemCompat.getActionView always returns null

CursorTreeAdapterのsetChildrenCursorで落ちるのを回避してみる

(この対処が正しいのかはわかりません)

落ちるところ

ExpandableListView+CursorTreeAdapter+LoaderCallbacksで実装していて、

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        adapter.setChildrenCursor(ID, data);
    }

setChildrenCursorしようとするとNullPointerExceptionが出ることがある。

CursorTreeAdapter.javaの実装を見てみると、

    public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
        
        /*
         * Don't request a cursor from the subclass, instead we will be setting
         * the cursor ourselves.
         */
        MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);

        /*
         * Don't release any cursor since we know exactly what data is changing
         * (this cursor, which is still valid).
         */
        childrenCursorHelper.changeCursor(childrenCursor, false); // <- ここで落ちる
    }
    synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
        MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
        
        if (cursorHelper == null) {
            if (mGroupCursorHelper.moveTo(groupPosition) == null) return null; // <- nullを返すことがある
            
            final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
            cursorHelper = new MyCursorHelper(cursor);
            mChildrenCursorHelpers.put(groupPosition, cursorHelper);
        }
        
        return cursorHelper;
    }

childrenCursorHelpernullになることがあり、changeCursor()を呼び出すのでNullPointerExceptionが出る。

・・nullチェックされていなくて涙目です。

が、nullになる状況を作り出している私のコードに問題があるのかもしれません。
(まだわかってない)

対処

以下のような応急処置をしました。

先にGroupCursorの生存を確認してから、setChildrenCursorするようにした。

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {

        Cursor groupCursor = adapter.getGroup(ID);

        if (groupCursor == null) {
            return;
        }

        adapter.setChildrenCursor(ID, data);
    }

getGroupの中身は・・

    public Cursor getGroup(int groupPosition) {
        // Return the group Cursor pointing to the given group
        return mGroupCursorHelper.moveTo(groupPosition);
    }

先ほどの「nullチェックされてなくて涙目」な部分を、
「自分で事前にチェックしてみた」ということになった。

NullPointerExceptionをcatchしてやりすごすよりは、良いかな??

コントロールセンターとAVAudioEngine

AVAudioEnginestart/stop
コントロールセンターのplay/pauseと同期してる模様。

コントロールセンターでpauseしても「▶︎」に変わらなくてあれーと思っていたら
Engineをstopしたらちゃんと動作した。

AVAudioEngineは都度start/stopするのが正解なのかー。

MoreのtintColor

storyboardのGlobal Tintを設定しても、「More」と「Edit」にその色は反映されず。
TabBarControllerをカスタムしているせいなのかもしれないけど・・。

MoreNavigationControllerにはtintColorを設定できても、
More画面やEdit画面そのものには設定できないので
半ばやけくそ気味にAppDelegate.m

[self.window setTintColor:[UIColor ...]];

としたらMoreもEditも変わってた。ふーむ・・