📝

flutterでテストコードを書く理由

に公開

はじめに

この記事は著者が調べ個人的にまとめたものとなります。そのため、誤りが含まれている場合がございます。もし誤りを見つけた場合はコメント等で教えていただけますと大変ありがたいです。

また、この記事は書いたものの色々あり下書きに眠っていた記事となります。
そのため状況が変わっている可能性がありますので、そちらも見つけた場合はコメント等で教えていただけますと大変ありがたいです。

テストコードとは何か。

テストコードとは「ソフトウェアの特定の機能や挙動が期待通りに動作するかを確認するために記述されたコード」のプログラムコードです。

開発の流れは次の図で示した「要件定義」「システム設計」「開発」「テスト」「移行・リリース」の5つのプロセスがあります。
開発の流れ
図1.開発の流れ

テストコードはテスト工程において作業が自動化され早期のバグ検出やコード変更の影響の把握、リファクタリングのサポートや継続的な品質保証を行うことができます。
テストコードの影響工程
図2.テストコードの影響工程

では、なぜテスト工程において手動でテストを行うのではなく、テストコードを書くのでしょうか。

なぜテストコードを書くのか。

flutterでプロダクトを制作した方がいれば必ず目にするカウントアップアプリがあります。

このアプリケーションでは画面右下にあるプラスボタンを押すことにより、数字がカウントアップします。
カウントアップアプリ

では、仮に上司や顧客に「このプラスボタンは10000回以上でも正しく動作するか調べて欲しい。」と言われた場合どのように確かめることができるでしょうか。

まず思いつく方法としてボタンを10000回押して手動で試すがあります。
しかし、正確に1プッシュ2プッシュと数え間違いをせずに10000回ボタンを押すことはできるのでしょうか。

おそらく慎重にカウンターなどを用いて押していけばいつかは成功するかもしれませんが、手動でやるには膨大な数で間違いが起こりやすいことは容易に想像つくと思います。また、ボタンを押すのに気を取られてしまうとバグなどを見落としてしまうかもしれません。

しかし、テストコードを書くと高速、かつ正確に10000回ボタンを押すことができるのかを試すことができます。

テストコード

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // MyAppをビルドする
    await tester.pumpWidget(const MyApp());

    // カウンターの一番最初の数字が0である。
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);
 
    for (int i = 0; i < 10000; i++) {
    // '+'アイコンをタップする。
      await tester.tap(find.byIcon(Icons.add));
      
        // 画面を再描画する。
      await tester.pump();

   // カウンターの数字が増えていることを確認する。
      expect(find.text((i + 1).toString()), findsOneWidget);
    }
  });
}

実行結果

このことから、テストコードを用いテストを自動化した方がより高速かつ正確に不具合がないかを調査することができます。

どのようなコードにテストコードを書くのか

テストコードを書くとなるとコード量が増えるためそれに伴い工数も増加します。そのため、すべてのコードに対して限られた工数でテストコードまで作成するのはなかなか容易ではありません。では、どのようなコードに対しテストコードを書く必要があるのでしょうか。

1.自信が持てないコード,不安が残るコード

プログラムを書いているとどうしてもこのコードこれで大丈夫なのかな。と思う時があると思います。

その際に「手動で試したら動いたからいっか。」としてしまうと、実は手動で試した条件下では動いたが、リリースした後に他の条件では動かなかった。ということが起こり得てしまいます。

そうした環境依存、条件依存による見落としを避けるためにテストコードを書くことができます。テストコードを用いて、さまざまな条件でプログラムを試すことにより、より正確性が上がり自身の書いたコードに自信を持つことができます。

2.プロダクトにとって重要度が高いコード

プログラムを書いていると、セキュリティ面やプロダクトのサービス内容等の様々な面において重要度が高い場面に対してプログラムを書くことがあります。

そうした時に、重要度が高い場面で実行エラーが起きてしまったり、想定していた挙動と異なる挙動をしてしまってはプロダクトとしての信頼性や価値が下がってしまうことがあります。

テストコードを書くことにより、そういった重大なエラーを防ぐことができます。
具体例を用い、もう少し具体的に説明します。

1.セキュリティ面で重要なところ

次に示す例のようにセキュリティ面において個人情報や外部に漏れてはいけない情報が漏れてしまってはプロダクトや会社の信頼が下がってしまいます。

  • プロフィール設定画面を押すと他のユーザーのプロフィールやメールアドレスが表示されるようになっていた。
  • 権限に関するプログラムが誤っており、外部のユーザーから社内の機密情報が見れるようになっていた。

テストコードを作成することで表示するべき情報が表示されているかを確認することができ、不具合を未然に防ぐことができるため、セキュリティー面の不安を動作確認だけでなく客観的に下げることができます。

2.不具合が起こると重大なトラブルに繋がりかねないもの

例のように想定していない挙動が起こったことにより金銭的トラブルに陥ったり、多方面に不利益を被らせてしまうおような挙動が起こってはセキュリティ面と同様に会社の信頼が下がってしまいます。

  • 決済システムを通して決済するとなぜか二重に引き落としをしてしまう。
  • 確認メールが送信したいメールアドレスではなく別のメールアドレスに送信してしまう。

テストコードを作成することにより、決済やメール送信が正しくできるかの確認をすることができるため、不具合を未然に防ぐことができ、金銭等のトラブルを防止することができます。

3.プロダクトにとって必須となるプロセス

例のように不具合が生じるとプロダクトにおいてのユーザーの利用の妨げになったり、影響範囲が広くプロダクトとして正常とは言い難い状況に陥ってしまうことがあります。

  • 戦闘システムに不具合が生じ、敵が倒せなくなっていた。
  • 商品一覧ページに不具合が生じ、商品詳細ページまで辿り着かなくなっていた。

プロダクトにおいてユーザーが必ず利用する部分に対しテストコードを作成することにより、プロダクトの主要部分に不具合がないか確認することができ、滞りなくユーザーに使用してもらうことができます。

3.手作業で動作確認することが難しいコード

プロダクトの中には以下の例のように手作業で動作確認を行うには時間がかかったり、データ等を変更する必要が出てくるものがあります。

  • アカウント作成などのデータベースに変更がかかってしまうもの
  • 日時や天気などの外部の情報に依存しているもの

外部データやデータベースに依存したりする動作確認を行うには手間がかかるコードに対し、テストコードを用いてテストをすることによりデータベースやパソコンの設定を変更したりしなくても不具合がないか確認することができます。

また、例外処理などの手動で起こすには少しハードルが高いものに関してはモックを使いわざと例外を発生させることによりテストを行うことができます。

4.将来的に問題・不具合を起こしそうなところ

たとえば、特定のライブラリに依存しておりアップデートによっては動かなくなる可能性のあるものや、複雑なコードになっており一部変更するとすべてのプログラムが正常に動作しなくなる恐れのあるものは事前にテストコードを書いておくと、その箇所を変更し不具合が起きた際にすぐに見つけることができます。

また、テストコードを書いておくことで次に仕様変更する人がテストコードを読みコードの意図や仕様を理解する手助けをすることもできます。

テストにはどのような種類があるのか

flutterには大きく3つのテストといくつかの継続的インテグレーション(CI)サービスがあります。この章ではテストの種類とそれぞれのテストの役割、そしていくつかの継続的インテグレーションサービスについて説明していきます。

ユニットテスト

ユニットテストとは1つの関数や1つのメソッド、1つのクラスなど分離できる最小単位でテストを行うことを指します。目的としてはメソッド等が異なる条件下で正しく機能するかを検証することです。これによりメソッド全体が期待通りに動作をするか確認することができます。このテストでは外部から受け取る情報はモック化したものを利用し、実際にユーザーが入力したり結果が画面に表示されたりすることはありません。

メリット

テスト範囲が限られるため不具合が起こったとしても原因の特定を容易に行うことができます。また、リファクタリングを行う際に外部動作に影響がないかを検証しながら作業を行うことができます。

デメリット

最小単位で分割するため全ユニットをテストしようとすると、膨大なテストコードを書く必要があり工数を増加させる可能性があります。

また、テストコード自体が誤っていた場合、正しいテスト結果を返すことができないため開発者を混乱させることがあります。

ウィジェットテスト

ウィジェットテストでは特定のウィジェットに対してテストを行う手法です。目的としてはウィジェットが入力や処理に対して正しく動作するかを確認するためです。これにより、ユーザーが操作した際に期待通りに動作するかを確認することができます。

このテストではテストを行いたいウィジェットに複数のクラスが関与する際、適切にウィジェットの状態や呼び出されているメソッド、環境を提供する必要があります。

メリット

画面全体ではなくウィジェット毎にテストを行うためユニットテストのみでは見つけることのできなかったユーザーの操作が起因の不具合や画面とメソッドの組み合わせによって起こるエラーを早急に見つけることができます。

デメリット

UIに微弱な修正があった際にテストコードを修正する必要があリます。また、テストコードだけで完全なUIを再現することが難しいため、一部のUIに対してテストを行うことができないことがあります。

インテグレーションテスト

インテグレーションテストとはアプリの全体に対しテストを行う手法です。目的としては各ウィジェットや各メソッドが組み合わ去った際に正しく動作するかを確認するためです。これにより、アプリ全体を通してウィジェットやメソッドの組み合わせにより起こる意図していない挙動がないかを確認することができます。このテストは実際のデバイス上でアプリケーションを実行する際のパフォーマンスを試すため実機もしくはエミュレータ上で実行されます。

メリット

アプリ全体の動作をテストするため、各メソッドや各ウィジェットの相互作用による不具合の発見することができます。また、デバイスやエミュレータ上で実行されることによる本番環境に近い状況でのテストが行えるということがあります。

デメリット

テストのセットアップや実行に時間がかかってしまうため頻繁な実行が難しく問題が発生した場合に、不具合が生じたメソッドやウィジェットを絞り込むことが難しいため修正に時間がかかることがあります。

継続的インテグレーションサービス

継続的インテグレーションとはコード変更をpushする際にテストを自動的に実行することでコードの品質を確保しながら開発スピードを向上させるdevOpsソフトウェア開発手法です。

メリット

プッシュ直後に自動的にビルドとテストが行われるため迅速なフィードバックを得ることができ、不具合を早い段階で発見することができます。また、マスターブランチにマージされる前にテストが行われることにより、マスターブランチが常に安定するということがあります。

デメリット

適切なテスト設計が必要であり、設定等の際にヒューマンエラーが起こる可能性があります。また、テストには完了までに時間を要するためその間にコードの変更が進行することがあります。

Discussion