kariadの戯言

思ったこととか感じたこととか何か書きます

テストエンジニアの観点を持ってテスト書いていくぞな話

ごきげんよう。かりあどです。

12/15~16に開催されたWACATE2018 冬 〜C’mon baby ジドウカ〜 に参加してきました。
そこでテストエンジニアの方々と色々議論できたのでそれを書きます。
またt_wadaさんによる講演もとても素晴らしかったので感想を書きます。
WACATEそのものの感想よりも議論の内容を書きたいので、WACATEって何?という方はこちら。

wacate.jp

当初の予定は参加レポートだったのですが、議論した内容があまりにもよかったのと、個人的に振り返りたかったので急遽予定変更です。

なおこちらの記事はOisix ra daichi Inc. Advent Calendar 2018の18日目となります。 前日の弊社スーパーSREエンジニアの@morihaya55さんの記事はこちら

adventar.org

開発者とテストの関わりについて

私は普段iOSアプリの開発をしているのですが、テストコードも書いています。
しかしいつも思っているのがテストコードはこれで足りているのか、どこまでかけば品質の担保となるのだろうかと考えていました。
実際に世の中的にはどうなんでしょうか? 開発者でテストを書いている方は何を基準にテストコードを書いているか気になります。

最近ではテストコードを開発者が書くことも一般的になってきたんじゃないかなと思う中で、じゃあ次のステップはと考えたときにそのテストコードにソフトウェアテストの観点が入っているのだろうか、ということじゃないかなと私は考えています。

そんな中でもっと自分自身ソフトウェアテストを勉強する、テストエンジニアと色々議論してみたくてWACATEに参加しました。

開発者視点、テストエンジニア視点

WACATEでは夜の分科会という話したいテーマごとに別れて議論する場があります。 そこで私はBDDのモブプログラミングに参加しました。 元々テストコードを書くにあたってテストエンジニアの観点を知りたかった私にはぴったりなテーマでした。 その場では時間があまりなく、多くは議論できなかったのですが、その後の延長戦で深夜の分科会としてさらに少人数で続きをやりました。 そしてなんとそこからはt_wadaさんも参加してのモブプログラミングとなりました。

ホワイトリストブラックリスト

深夜の分科会でまず議論となったのが以下のようなリファクタリングを行った際のことでした。*1

 自動販売機の入金確認
 GIVEN 自動販売機に
 WHEN 1円を入れた時
 THEN 入金額が0円であること

リファクタリング

public class VendingMachine {
  int currentMoney = 0;

  public void insertCoin(int money) {
    if (money == 1) {
      return;
    }
    currentMoney += money;
  }
}

リファクタリング

public class VendingMachine {
  int currentMoney = 0;

  public void insertCoin(int money) {
    if (money != 1) {
      currentMoney += money;
    }
  }
}

まず第一段階として、 != の否定が入ると読み辛いのではないか、という意見が出ました。 が、ここで別の視点でこのリファクタリングはあまり良くないという意見が出ました。

とのことでした。

先に謝ります。ごめんなさい、改めてコードを見てみたらこのコードに対してどっちがホワイトリストブラックリストなのかちょっと混乱してきました。 もしお分かりになる方いましたら教えていただけないでしょうか。

しかし、ホワイトリストのが頑強であるというのは理解できます。 意図しない入力に対して、処理を実行してしまうことはエラーの原因になり得ます。 このホワイトリストの考え方はでも同じことが言えるのではないでしょうか?*2

少し考えてみます。例えば次のコードはどうでしょうか。(ここからSwiftで書きます)

enum Coin: Int {
    case hundred = 100
}

class VendingMachine {
    var money: Int = 0
    
    func insert(coin: Coin) {
        money += coin.rawValue
    }
}

こちらは insert 関数ではそもそも Coin 型しか受け付けていません。 つまりそれは許可したもののみ受け付けるホワイトリストを活用している例でしょう。 型を用いたホワイトリストは意図しない入力に対してはコンパイル時にエラーとなる、かなり早いフィードバックです。

一番最初のJavaで書いていたプログラムでは、insert 関数はint型を引数に取っていました。この時点でinsert関数に対するテストは入力に取りうる値がint型全てになったしまったことがわかります。

一方で型を活用したSwiftのプログラム*3では入力に取りうる値がCoin型に限定されています。そのため、テストする範囲もCoin型で定義されたcase数で済みます。

型を活用したホワイトリストはただ頑強であるだけではなく、型検査そのものがテストの役割を果たしているため意図しない入力に対するテストを実施しなくてもよくなります。 もちろん一番最初の入力はInt型かもしれません。その時でも以下のようにCoin型をinitializeするタイミングで存在しない値が引数として渡されて時点でエラーとすることができます。

let input: Int // 何かしらの入力と仮定
guard let coin = Coin(rawValue: input) else {
    throw InvalidInputError.notCoin
}

多言語には明るくありませんが、Swiftのenumはテスト観点からみてもとても強力だと思います。

これからもなるべく型を意識したプログラミングは続けていきたいところです。

そのテストケース、どう書きますか

リファクタリングではテスト概要のリファクタリングも行いました。 元のテストケースは次になります。

 自動販売機の入金確認
 GIVEN 自動販売機に
 WHEN 100円を入れた時
 THEN 入金額が100円であること

 自動販売機の入金確認
 GIVEN 自動販売機に
 WHEN 100円を入れた
 AND 50円を入れた時
 THEN 入金額が150円であること

ここから1段階目のリファクタリングを行います。

 自動販売機に1枚コインを入れた時の入金確認
 GIVEN 自動販売機に
 WHEN 100円を入れた時
 THEN 入金額が100円であること

 自動販売機に2枚コインを入れた時の入金確認
 GIVEN 自動販売機に
 WHEN 100円を入れた
 AND 50円を入れた時
 THEN 入金額が150円であること

より具体的なテスト概要になったことがわかります。 が、ここでさらなる指摘がテストエンジニアのみなさんから上がりました。

この場合コインを3枚入れたらどうなりますか?

言われてみて気がつくわけです。1枚、2枚とテストケースが来たら当然3枚は、4枚はとなります。 が、一方で私の頭の中では次のようになっていて、実際に先ほどの問いにも次のように答えました。

コードでは加算しているのだから何枚来ても変わらないです

開発者である私はコードを起点に考えていました。 そのため、1枚、2枚というところには注意が向かず、コード上では何枚来ても同じだから2枚でテストしておけば問題ないと考えていたわけです。

一方でテストエンジニアの方は、このテストケースをみて、3枚の場合がテストされていない=バグが潜んでいるかもしれないと考えたことになります。 もちろんここではモブプロなのでコードを書くところもみていた為、何枚来ても同じということはテストエンジニアも知っていました。 しかし、その上で第3者視点でこのテストケースだとそれがわからない、という観点を持っていたことになります。 最終的にその場では次のようになりました。

 自動販売機に連続して複数枚コインを入れた時の入金確認
 GIVEN 自動販売機に
 WHEN 100円を入れた
 AND 50円を入れた時
 THEN 入金額が150円であること

よくテストコードは仕様にもなる、と言いますが仕様書という観点でもリファクタリング後の方が実装との紐付けがしやすくなっているようにも思えます。

これからはテストを説明する自然言語部分にも注意してテストコードを書いていかねばと思わされました。

テストエンジニアと開発者によるモブ(ペア)プロ

これらの経験を経て、テストコードを書く際にもソフトウェアテストの観点を取り入れることでより良いテストコードになっていくと確信しました。 そうした中でテストエンジニアと開発者の技術的交流や一緒に働くことがもっと盛んになった方がいいのではないか、という気もしてきました。

特に私が経験してきたアジャイル開発では厳密なテストフェーズというものがない場合が多く、開発者の書くテストコードで品質を担保しなくてはいけないパターンが多くありました。 そうした中で同じチームにテストエンジニアに入ってもらい、一緒に働くことで早い段階からテストエンジニアの観点を持って開発することができます。 もしくは、開発者自身がソフトウェアテストを勉強し、テストエンジニアの観点を身につけていくのも同じ結果を期待できるのではないでしょうか。

TDDのその先へ、ChekingからTestingへ

今回のWACATEではあのt_wadaさんによる講演も開催されました。 その内容はとてもよく、Twitter上では感動して涙がでたという人も一人ではありませんでした。

私もTDDBootCampへの参加経験などはあったのですが、t_wadaさんの講演を聞くのは初めてでした。

TDDの説明をすることが目的ではないので飛ばしますが、これは絶対読んだ方がいいです。

テスト駆動開発

テスト駆動開発

内容は一般的なFizzBuzzでした。 それをTDDやコードを知らない人でも理解できるようにとても丁寧に説明しながら進行していきました。
ある程度進んだところで次のようなコードになっていました。(当日はJavaでしたがSwiftで書きます)
そこから説明していきます。

class FizzBuzzTest: XCTestCase {
    let fizzBuzz = FizzBuzz()

    func test_3の時Fizzと返される() {
        XCTAssertEqual(fizzBuzz.fizzBuzz(3), "Fizz")
    }

    func test_6の時Fizzと返される() {
        XCTAssertEqual(fizzBuzz.fizzBuzz(6), "Fizz")
    }

    func test_5の時Buzzと返される() {
        XCTAssertEqual(fizzBuzz.fizzBuzz(5), "Buzz")
    }

    func test_15の時FizzBuzzと返される() {
        XCTAssertEqual(fizzBuzz.fizzBuzz(15), "FizzBuzz")
    }
}

少し過程を説明しますと、ToDoリストからテストを書いていき、3の倍数は三角推論を用いて2つのテストケースを書いています。
一方で5の倍数や3と5の倍数のテストや実装は明白な実装を用いて1つのテストケースしか書いていません。

ここまでは通常のTDDでも到達するところかと思います。
講演ではさらにここにソフトウェアテストの観点を入れて品質保証のためのテストが追加されていきました。

まずはToDoリストをみてみます。

  • [x] 3の倍数の時Fizzが返される

    • [x] 3の時Fizzが返される
    • [x] 6の時Fizzが返される
  • [x] 5の倍数の時Buzzが返される

    • [x] 5の時Buzzが返される
  • [x] 3と5の倍数の時はFizzBuzzが返される

本来の仕様に加えて、さらに問題を小さくするために具体的な数字に落としています。
その具体的な数字でテストコードも書かれているため、本来の仕様が抜け落ちてしまっています。 それでは後日別の人が見たときに仕様がすぐわからないかもしれません。
そのため次のような構造的なテストケースにリファクタリングします。

class FizzBuzzTest: XCTestCase {
    let fizzBuzz = FizzBuzz()

    func test_3の倍数の時Fizzと返される() {
        XCTContext.runActivity(named: "3の時Fizzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(3), "Fizz")
        }

        XCTContext.runActivity(named: "6の時Fizzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(6), "Fizz")
        }
    }

    func test_5の倍数の時Buzzと返される() {
        XCTContext.runActivity(named: "5の時Buzzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(5), "Buzz")
        }
    }

    func test_15の倍数の時FizzBuzzと返される() {
        XCTContext.runActivity(named: "15の時FizzBuzzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(15), "FizzBuzz")
        }
    }
}

抜け落ちていた仕様を構造化することでテストコードに落とし込むことができました。*4

実行結果の見た目もいい感じです。 f:id:kariaduu:20181218223604p:plain

しかしまだこれでは足りていないところがあります。
3の倍数は複数のパターンでテストしていますが、5の倍数や3と5の倍数では1つのパターンでしかテストしていません。
何かしらの変更があった際に気が付けない可能性もあります。 次のようにさらにリファクタリングしていきます。

class FizzBuzzTest: XCTestCase {
    let fizzBuzz = FizzBuzz()

    func test_3の倍数の時Fizzと返される() {
        XCTContext.runActivity(named: "3の時Fizzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(3), "Fizz")
        }

        XCTContext.runActivity(named: "6の時Fizzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(6), "Fizz")
        }
    }

    func test_5の倍数の時Buzzと返される() {
        XCTContext.runActivity(named: "5の時Buzzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(5), "Buzz")
        }

        XCTContext.runActivity(named: "10の時Buzzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(10), "Buzz")
        }
    }

    func test_15の倍数の時FizzBuzzと返される() {
        XCTContext.runActivity(named: "15の時FizzBuzzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(15), "FizzBuzz")
        }

        XCTContext.runActivity(named: "30の時FizzBuzzと返される") { _ in
            XCTAssertEqual(fizzBuzz.fizzBuzz(30), "FizzBuzz")
        }
    }
}

それぞれのケースに追加しました。 これらはTDDのCheckingとしては書く必要のないテストだったかもしれません。 一方でTestingの観点からはあったほうが良いテストケースだったでしょう。 実際には10や30といった元の数字の2倍ではなく、異なった数字でテストした方がいいかもしれません。

ここまでリファクタリングを重ねたことで、本来のTDDに品質保証の観点を取り入れることができました。 TDDのその先をみた気分になりとても感動し、実践していきたいと思いました。

まとめ

3つほど議論したことや、講演を説明しましたが全てに共通していることは開発にテストエンジニアの観点を取り入れることの重要性です。
冒頭でも述べましたが、自動テストのコードを我々開発者が書くことが一般化してきた今だからこそ、開発者がテストエンジニアの観点を持って品質保証まで考えられたテストコードを書いて行くべきだと私は思っています。 テストコードには開発を駆動させる役割もあります。これらをうまく使っていけば開発を駆動させながらさらに品質保証まで行うことができます。*5 今回私はそれをテストエンジニアと一緒にコードを書いていくことで学ぶことができました。 最近では開発者の間でもテストへの関心が高まっていると個人的には感じています。 そこでぜひおすすめしたいのはテストエンジニアと交流し一緒にコードを書くことです。 今回の経験を経て、強くそれを感じています。 そして私たち開発者がテストエンジニアの観点を持ってテストコードがかけるようになっていきましょう。

おまけ

まったく参加レポートしていなかったので最後に一つだけ。

BPP(ベストポジションペーパー賞)というものをもらってしまいました。 ポジションペーパーとは、参加申し込みのときに書いた自己紹介やら意気込みやらを書いたものになります。 BPPは参加者投票でもっとも良いと思ったポジションペーパーが選ばれます。 私は何を書けばいいかわからず、困った結果、お酒を飲んで勢いで書きました。 どこが良くて参加者みなさまに投票していただいたのかはわかりませんが投票していただいたみなさまありがとうございます。

ちなみに私が書いたポジションペーパーが次のようなものです。もし次回以降書く内容に困った方がいたら参考にはならないかもしれませんがないよりあった方が雰囲気つかめると思うので置いておきます。

f:id:kariaduu:20181218231145p:plain

最後にこれも置いておきます。一緒にモリモリテストやっていってくれる方きてくれると嬉しいです。

recruit.oisixradaichi.co.jp

*1:記憶を頼りに書いているので実際とはおそらく少し異なっています

*2:私は普段Swiftを書いているため強い静的型付け言語を基準にしがちです

*3:このように書くとJavaが悪いように見えてしまうかもしれませんが、単純にWACATE時はJavaで書き、説明には普段から慣れ親しんでいるSwiftで書いているというだけです。

*4:Javaではクラス毎に分けていました

*5:テストコードで全ての品質保証ができるわけではありません