yaakaito.org

いい感じのViewControllerのテストの書き方を考えてみる

Testing, iOS

こんにちは!うきょーです! いろいろあって落ち着いてなかったのでブログ全然書けてなかった!

すごい昔の話しだけど、TwitterみてたらViewControllerのテストがあーだこーだあったので自分の考えを書いてみるよ。 前回は手法的な話だけをしたから、今回は実際どう書くのがいいの的な話。 モックとかの使い方は分かってる前提&一旦非同期はないものとして考えて書いてみる。

ViewControllerの実装はほぼプライベートなんだけど的な話

あってると思う。それで正しい。 ただViewControllerの利用者はフレームワークなので、例えばボタンをタップしたときにどれを呼び出すかというポインタをフレームワークは知っている。 なのでテストがプライベートにアクセスしたりするのは結構正しくて、テストはフレームワークのフリをして書けばよいと思う。

自作自演っぽくなる

これに関しては多分テストする単位が小さいんじゃないだろうかと思う。 シナリオ単位でテストするのがよいと思っている。 単純にメソッドが動いてるーはあんまり意味がない感じがする。

いわゆる単体テストではないのでは説

あってそう、テストの性質が異なっているのは確か。 そういう場合はテストフレームワークを分けるといいとかって話もあるし、KIFとか使って書くのでもいいんじゃないかな。 ただ、ViewControllerのテスト、というと感覚的にこの2つの中間くらいの話なのかなって気もするので、そこまで大げさにしなくてもいいのかなと思う。 モデルのテストはSenTestingKitだけど、ViewControllerのテストはSpecta、みたいなのもわりとありかなと思ってる。

というわけで例

ログイン画面とかで考えてみる。

ありそうな流れ

  • ログインが必要になると、ログイン画面が表示される
  • ユーザーがidとパスワードを入力
  • サーバーに問い合わせる
  • 成功したら「ログインした」とか出して、画面を戻す
  • 失敗したら「ログインに失敗した」とか出して、もう一度入力させる
    • この時にパスワードはクリアされるとユーザーに優しそう
    • パスワードが間違っていますとか、赤字で残ってるとユーザーに優しそう

みたいな感じですかね。

テストのシナリオ

画面だすところは別のViewControllerのテストかと思うので、そこは省く。

こんな感じだろうか。この辺を書いておけばいい感じになるような気がする。

成功

  1. idとパスワードを入力
  2. サーバーに問い合わせる(フリをする)
  3. ログインに成功する
  4. 「ログインした」と表示される
  5. 画面を戻す

失敗 => 成功

  1. idとパスワードを入力
  2. サーバーに問い合わせる(フリをする)
  3. ログインに失敗する
  4. 「ログイン失敗した」と表示される
  5. エラー画面になる
  6. 再度パスワードを入力
  7. サーバーに問い合わせる(フリをする)
  8. ログインに成功する
  9. 「ログインした」と表示される
  10. 画面を戻す

失敗 => 失敗 => …

  1. idとパスワードを入力
  2. サーバーに問い合わせる(フリをする)
  3. ログインに失敗する
  4. 「ログイン失敗した」と表示される
  5. エラー画面になる
  6. 再度パスワードを入力
  7. サーバーに問い合わせる(フリをする)
  8. ログインに失敗する
  9. 「ログイン失敗した」と表示される
  10. エラー画面になる

いい感じに見せる方法を考えてみる

ヘルパーメソッドを作ってそれっぽく見せる

スタブに毎回ユーザー入力のフリを直接いれていくのも芸がない(というか意図が伝わり難い)ので、

1
[self inputId:@"yaakaito" andPassword:@"hogefuga"];

こういう感じにするとよいんじゃないかな。

画面が変わる単位でまとめる

今回の場合はエラー画面なんだけど、毎回エラーと表示された、パスワードがクリアされた、とかアサーションするのはだるい。 こういうことをしたときに、エラー画面になるよ、という風にかけるといいんじゃないかと思う。

1
2
3
4
[self shouldSeeErrorIf:^{
    [self inputId:@"yaakaito" andPassword:@"invalid"];
    [self confirm];
}];

こんな感じ。

状況によってはカテゴリでファサードみたいなのするとよさそう

上と似たような感じだけど、状況によってはViewControllerをテスト用に拡張するとよいかもしれない。 今回はこっちは考えずに一旦は上の2つでやってみる。

XUnitと仮定してざっと書いてみる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)testLoginSuccess {
    [self shouldSeeSuccess:^{
        [self inputId:@"yaakaito" andPassword:@"valid-password"];
        [self confirm];
    }];

}

- (void)testLoginFailureAndSuccessOnRetry {

    [self shouldSeeError:^{
        [self userInputId:@"yaakaito" andPassword:@"invalid-password"];
        [self userConfirm];
    }];

    [self shouldSeeSuccess:^{
        [self inputId:@"yaakaito" andPassword:@"valid-password"];
        [self confirm];
    }];
}

- (void)testLoginFailureAndFailureOnRetry {

    [self shouldSeeError:^{
        [self userInputId:@"yaakaito" andPassword:@"invalid-password"];
        [self userConfirm];
    }];

    [self shouldSeeError:^{
        [self inputId:@"yaakaito" andPassword:@"valid-password-2"];
        [self confirm];
    }];
}

それっぽい!

XSpecだと仮定してざっと書いてみる

KiwiやSpectaだとselfがないので、テスト用のオブジェクトとか作ればいいんだろうか? 今回は雰囲気がみたいので、コンパイルできないけどselfで書いてみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
describe(@"Login Controller", ^{
    describe(@"Login", ^{
        context(@"When login succeed", ^{
            it(@"should display Success message", ^{
                [self shouldSeeSuccess:^{
                    [self inputId:@"yaakaito" andPassword:@"valid-password"];
                    [self confirm];
                }];
            });
        });

        context(@"When login failed", ^{
            it(@"should display Failure message", ^{
                [self shouldSeeError:^{
                    [self userInputId:@"yaakaito" andPassword:@"invalid-password"];
                    [self userConfirm];
                }];
            });

            context(@"But, retry it and succeed", ^{
                it(@"should display Success message", ^{
                    [self shouldSeeError:^{
                        [self userInputId:@"yaakaito" andPassword:@"invalid-password"];
                        [self userConfirm];
                    }];
                });
            });

            context(@"But, retry it and failed", ^{
                it(@"should display Failure message", ^{
                    [self shouldSeeError:^{
                        [self userInputId:@"yaakaito" andPassword:@"invalid-password"];
                        [self userConfirm];
                    }];
                });
            });
        });
    });
});

英語があっているかすごく微妙だけど、なかなかよいんじゃないか?