Test

なぜUnitTestが書けないのか?〜今日から始める品質改善:基礎編〜

1. はじめに

「UnitTest、大事なのはわかるけど時間がないし、どこまでどう書けばいいのか、よくわからない…」

この記事は、UnitTest未経験の方向けに執筆しました。

UnitTestを書くことの重要性・メリット、
実際に書き始めるためのステップまでを簡単にまとめてみました。

2. なぜUnitTestを書かないのか?

  • 「UnitTestを書く時間がない」
    • 短期的には時間がかかるように見えるかもしれませんが、バグ修正や手戻りを減らし、長期的には開発時間の短縮につながります。

  • 「テストはQAチームがやる」
    • QAチームによる試験は最終的な品質保証であり、開発者自身がUnitTestで早期にバグを発見することで、より高品質なコードを作成できます。

  • 「テストを書くのが難しい・メリットがわからない」
    • 後述のセクションで、簡単なサンプルをもとにUnitTestを書くことで得られるメリットを解説します。簡単なテストから初めて、徐々に慣れていきましょう。

  • 「コードの依存関係が強い」Or「テストに必要なモックやスタブの作成が難しい」
    • 特定の箇所をテストしようとしても、他の多くの部分に依存していて、テストが困難になっている。
    • 外部サービス連携部分をテストしたいが、モックやスタブの作成方法がわからない。
    • 応用編では、モックがテストを容易にするだけでなく、より良い設計にも繋がることを解説します。

3. UnitTestを書くことで得られるメリット

開発効率・品質向上

  • 早期にバグを発見し、リリース前の品質を高める
    • UnitTestは、コードの小さな誤りや実装上の設計ミスを早期に発見することができます。

具体的な例:サンプルコード

  • サンプルコードではシンプルな4つの計算関数について、それぞれ複数のパターンで期待通りの結果になるかを検証するテストコードを書いています。
  • UnitTestを書かない場合、サンプルのような小さなロジックでも、少なくとも以下の手間がかかります。
    • アプリをビルドして実行する
    • 関連する画面まで遷移する
    • 計算ロジックが実行される操作を行う
    • 結果を目視で確認する
  • UnitTestを書くことで、以下のようなメリットが期待できます。
    • XcodeなどのIDE上でテストを実行するだけで、計算ロジックの妥当性を検証できる
    • 複数のパターンをまとめてテストできる
    • テスト結果が自動的にIDE上に表示されるので、目視確認の手間が省ける

// Calculator.swift

import Foundation

class Calculator {
    // 2つの数値を足し算する
    func add(a: Int, b: Int) -> Int {
        return a + b
    }

    // 2つの数値を引き算する
    func subtract(a Int, b: Int) -> Int {
        return a - b
    }

    // 2つの数値を掛け算する
    func multiply(a: Int, b: Int) -> Int {
        return a * b
    }

    // 2つの数値を割り算する
    // 0で割る場合はエラーを返す
    func divide(a: Int, b: Int) throws -> Int {
        guard b != 0 else {
            throw CalculatorError.divisionByZero
        }
        return a / b
    }
}

// 独自のエラー型を定義
enum CalculatorError: Error {
    case divisionByZero
}

// CalculatorTests.swift

import XCTest
@testable import YourProjectName 
class CalculatorTests: XCTestCase {

    var calculator: Calculator!

    // 各テストケースの実行前に呼ばれる
    override func setUp() {
        super.setUp()
        calculator = Calculator()
    }

    // 各テストケースの実行後に呼ばれる
    override func tearDown() {
        super.tearDown()
        calculator = nil
    }

    // 足し算のテスト
    func testAdd() {
        XCTAssertEqual(calculator.add(a 1, b: 2), 3, "1 + 2 は 3 であるべき")
        XCTAssertEqual(calculator.add(a: -1, b: 1), 0, "-1 + 1 は 0 であるべき")
        XCTAssertEqual(calculator.add(a: 0, b: 0), 0, "0 + 0 は 0 であるべき")
    }

    // 引き算のテスト
    func testSubtract() {
        XCTAssertEqual(calculator.subtract(a: 5, b: 3), 2, "5 - 3 は 2 であるべき")
        XCTAssertEqual(calculator.subtract(a: 10, b: 5), 5, "10 - 5 は 5 であるべき")
        XCTAssertEqual(calculator.subtract(a: 0, b: 0), 0, "0 - 0 は 0 であるべき")
    }

   // 掛け算のテスト
    func testMultiply() {
        XCTAssertEqual(calculator.multiply(a: 2, b: 3), 6, "2 * 3 は 6 であるべき")
        XCTAssertEqual(calculator.multiply(a: -2, b: 3), -6, "-2 * 3 は -6 であるべき")
        XCTAssertEqual(calculator.multiply(a: 0, b: 5), 0, "0 * 5 は 0 であるべき")
    }

    // 割り算のテスト
    func testDivide() {
        XCTAssertEqual(try calculator.divide(a: 10, b: 2), 5, "10 / 2 は 5 であるべき")
        XCTAssertEqual(try calculator.divide(a: 15, b: 3), 5, "15 / 3 は 5 であるべき")
        // 0で割った場合にエラーが発生することを確認
        XCTAssertThrowsError(try calculator.divide(a: 10, b: 0)) { error in
            XCTAssertEqual(error as? CalculatorError, .divisionByZero)
        }
    }
}

4. どう始める?

まずは簡単なステップから

ステップ1:環境構築と簡単なテストの実行

    1. テストフレームワークの選定と導入
      • まずは使用するプログラミング言語に対応したテストフレームワークを選びます。
        • Swiftの場合、XCTestという従来のフレームワークの他に、
          2024年に登場したばかりのSwift Testingというフレームワークがあります。
      • 選んだフレームワークをプロジェクトに導入し、簡単なテストを実行できる状態にします。
        • プロジェクトにテストターゲットが作成されていれば、あとはテスト用のファイルを作成して、XCTestを使用する場合はimport XCTestを記述すればOKです(前述のサンプルコード参照)
    2. 簡単な関数のテスト
      • for文やwhile文などのループ処理を含む関数を選びます。
      • ループが正しく実行されること、ループの開始条件、終了条件が正しいことを検証するテストケースを作成します。

    ステップ2:既存コードに対するテストの追加

    1. 簡単な既存関数に対するテスト
      • プロジェクト内の比較的単純な既存の関数を選びます。
      • その関数に対するテストケースをいくつか作成し、テストを実行します。
      • 最初は、正常系のテストから始めてみましょう。
    2. 境界値テスト
      • 関数の引数として、通常ありえない値や、最大値、最小値に近い値などを与えて、関数が正しく動作するか確認します。
        • 年齢を引数とする関数なら、マイナス値や、150歳以上の値が入力された場合にどうなるかなどをテストします。

    ステップ3:少し複雑なロジックに対するテスト

    1. 条件分岐のある関数
      • if-else文などの条件分岐を含む関数を選びます。
      • それぞれの条件分岐が正しく動作することを検証するテストケースを作成します。
    2. ループ処理のある関数
      • for文やwhile文などのループ処理を含む関数を選びます。
      • ループが正しく実行されること、ループの開始条件、終了条件が正しいことを検証するテストケースを作成します。

    ステップ4:モックの使用

    • ※応用編で解説します※

    5. 注意すべきこと

    目的の明確化

    • UnitTestを書いたからといって、認識齟齬による仕様上のバグなどはもちろん防げないですし、
      複数のコンポーネントが連携して動作する場合のバグは検出しにくいです。
    • Unitestはあくまで個々のコンポーネントや関数が単独で正しく動作することを検証する手段であり、
      複数の要素やシステムが絡む内容の検証時は結合試験や総合試験を別途実施する必要があります。

    コストの意識

    • 長期的に見れば開発時間の短縮や品質向上に貢献するUnitTestですが、
      どうしてもUnitTest自体を記述する時間と労力やメンテナンスコストはかかります。
    • テスト対象を絞って重要な機能に集中するため、
      どこまでテストコードを記述するかはチーム全体のルール策定を行うのも手でしょう。

    品質の均一化

    • UnitTestは個人の経験や知識によって、どこまでどのように書くか、
      という点でバラつきが出やすい分野です。
    • 特定のメンバーだけが詳細なUnitTestを書き、他のメンバーは簡単なテストしか書かない
      といった状況を避けるため、チーム内でUnitTestの粒度や範囲について認識合わせを行いましょう。

    6. まとめ

    小さなステップから着実に

    • まずは小さな範囲からUnitTestを書き始め、徐々に慣れていきましょう。

    チームで共有・雰囲気づくり

    • チーム内でルール策定およびナレッジ共有を定期的に行い、「UnitTestを書く」文化を醸成しましょう。

    完璧より改善

    • 最初から完璧なテストを書くことは難しいです。まずは動くテストを書き、徐々に改善していくことを目指しましょう。


    各チームメンバーが自信を持ってテストコードを書けるようになるために、

    まずは小さなステップから始め、
    チーム全体でナレッジを共有しつつ、より良い状態にしていきましょう!


    Latest最新記事

    Popular人気の記事