テストの基本

編集

テストは、ソフトウェアの品質を保証し、期待通りに動作することを確認するプロセスです。効果的なテストは、バグの早期発見、コードの信頼性向上、リファクタリングの安全性確保に不可欠です。

テストの種類

編集

ユニットテスト

編集

個々の関数やメソッドの動作を検証します。

Java (JUnit) による例

編集
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    @Test
    void testAddition() {
        Calculator calculator = new Calculator();
        
        assertEquals(5, calculator.add(2, 3), 
            "2 + 3 should equal 5");
        
        assertEquals(0, calculator.add(-1, 1), 
            "Negative and positive numbers should balance");
    }
    
    @Test
    void testDivision() {
        Calculator calculator = new Calculator();
        
        assertEquals(2, calculator.divide(6, 3), 
            "6 divided by 3 should be 2");
        
        assertThrows(ArithmeticException.class, 
            () -> calculator.divide(1, 0), 
            "Division by zero should throw exception");
    }
}

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}

モック(Mock)テスト

編集

外部依存関係をシミュレートします。

Kotlin (Mockito) による例

編集
import org.junit.jupiter.api.Test
import org.mockito.Mockito.*
import org.mockito.kotlin.mock
import kotlin.test.assertEquals

class UserServiceTest {
    @Test
    fun `should create user with valid data`() {
        // モックの作成
        val userRepository = mock<UserRepository>()
        val emailService = mock<EmailService>()
        
        // スタブの設定
        `when`(userRepository.save(any())).thenReturn(true)
        
        // テスト対象のサービス
        val userService = UserService(userRepository, emailService)
        
        // テスト実行
        val user = User("john@example.com", "password123")
        val result = userService.registerUser(user)
        
        // 検証
        assertTrue(result)
        verify(userRepository).save(user)
        verify(emailService).sendWelcomeEmail(user.email)
    }
}

class UserService(
    private val userRepository: UserRepository,
    private val emailService: EmailService
) {
    fun registerUser(user: User): Boolean {
        // ユーザー登録のビジネスロジック
        val saved = userRepository.save(user)
        if (saved) {
            emailService.sendWelcomeEmail(user.email)
        }
        return saved
    }
}

// インターフェースの定義
interface UserRepository {
    fun save(user: User): Boolean
}

interface EmailService {
    fun sendWelcomeEmail(email: String)
}

統合テスト

編集

異なるコンポーネント間の相互作用を検証します。

TypeScript (Jest) による例

編集
import axios from 'axios';
import { UserService } from './UserService';

// 実際のAPIとの統合テスト
describe('UserService Integration', () => {
    let userService: UserService;

    beforeEach(() => {
        userService = new UserService(axios);
    });

    it('should fetch user details from API', async () => {
        // モック化されたAPIレスポンス
        const mockResponse = {
            data: {
                id: 1,
                name: 'John Doe',
                email: 'john@example.com'
            }
        };

        // axiosのモック
        jest.spyOn(axios, 'get').mockResolvedValue(mockResponse);

        // テスト実行
        const user = await userService.getUserById(1);

        // 検証
        expect(user).toEqual(mockResponse.data);
        expect(axios.get).toHaveBeenCalledWith('/users/1');
    });

    it('should handle API errors', async () => {
        // エラーシナリオのテスト
        jest.spyOn(axios, 'get').mockRejectedValue(new Error('Network Error'));

        // エラーハンドリングのテスト
        await expect(userService.getUserById(1))
            .rejects
            .toThrow('Failed to fetch user');
    });
});

// サービスクラスの実装例
class UserService {
    constructor(private httpClient: typeof axios) {}

    async getUserById(id: number) {
        try {
            const response = await this.httpClient.get(`/users/${id}`);
            return response.data;
        } catch (error) {
            throw new Error('Failed to fetch user');
        }
    }
}

プロパティベーステスト

編集

ランダムな入力で広範囲のテストを行います。

Scala (ScalaCheck) による例

編集
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll

object MathPropertyTest extends Properties("MathProperties") {
  // 可換性のテスト
  property("addition is commutative") = forAll { (a: Int, b: Int) =>
    a + b == b + a
  }

  // 結合法則のテスト
  property("addition is associative") = forAll { (a: Int, b: Int, c: Int) =>
    (a + b) + c == a + (b + c)
  }

  // 逆元の存在のテスト
  property("subtraction has inverse") = forAll { (a: Int) =>
    a - a == 0
  }

  // 複雑な関数のプロパティテスト
  def sortedListProperty(list: List[Int]): Boolean = {
    val sorted = list.sorted
    sorted.length == list.length &&
    sorted.forall(list.contains) &&
    (sorted === sorted.distinct)
  }

  property("sorting preserves list properties") = forAll { (list: List[Int]) =>
    sortedListProperty(list)
  }
}

ふるまい駆動開発 (BDD)

編集

Rust (Cucumber-like スタイル)

編集
// 特徴ファイル
Feature: User Authentication

Scenario: Successful user login
  Given a registered user with email "user@example.com"
  When the user enters correct password
  Then the login should be successful
  And a session token should be generated

// テストコード
#[cfg(test)]
mod authentication_tests {
    use super::*;

    struct AuthContext {
        user_email: String,
        password: String,
        login_result: Option<Result<String, AuthError>>
    }

    impl AuthContext {
        fn new() -> Self {
            AuthContext {
                user_email: String::new(),
                password: String::new(),
                login_result: None
            }
        }

        fn with_registered_user(&mut self, email: &str, password: &str) {
            // ユーザー登録のロジック
            self.user_email = email.to_string();
            self.password = password.to_string();
        }

        fn attempt_login(&mut self) {
            let auth_service = AuthService::new();
            self.login_result = Some(
                auth_service.login(&self.user_email, &self.password)
            );
        }

        fn assert_login_successful(&self) {
            assert!(self.login_result.is_some());
            assert!(self.login_result.as_ref().unwrap().is_ok());
        }
    }

    #[test]
    fn test_user_authentication() {
        let mut context = AuthContext::new();
        
        // Given
        context.with_registered_user("user@example.com", "correct_password");
        
        // When
        context.attempt_login();
        
        // Then
        context.assert_login_successful();
    }
}

テストの設計原則

編集
  1. 独立性:各テストは独立して実行可能であるべき
  2. 再現性:同じ入力に対して常に同じ結果を返す
  3. 網羅性:可能な限り多くのシナリオをカバー
  4. 簡潔性:テストは読みやすく、理解しやすいこと

テストの注意点

編集
  • テストコードも本番コードと同様に重要
  • 過度なテストは避ける
  • エッジケースを忘れずに
  • テストは継続的に更新する

テストの自動化

編集
  1. 継続的インテグレーション (CI) の活用
  2. テスト実行の自動化
  3. コードカバレッジの監視

まとめ

編集

テストは単なる品質保証ツールではなく、ソフトウェア開発の重要な設計手法です。適切なテスト戦略は、コードの信頼性と保守性を大幅に向上させます。