課題20: テストスイート構築

マンダトリー要件

問題1:テストの基礎理解(15点)

以下の質問に答えなさい。

  • テストの種類(5点)
- 単体テスト、統合テスト、ドキュメントテストの違いを説明しなさい。 - それぞれの配置場所と実行方法を述べなさい。

  • アサーションマクロ(5点)
以下のマクロの使い分けを説明しなさい: - assert! - assert_eq! - assert_ne!

それぞれの使用例を示すこと。

  • テスト属性(5点)
以下の属性の意味と使い方を説明しなさい: - #[test] - #[should_panic] - #[ignore] - #[cfg(test)]

問題2:電卓ライブラリのテスト(30点)

以下の電卓ライブラリを実装し、包括的なテストスイートを作成しなさい。

2.1 基本実装(10点)

// src/lib.rs

pub struct Calculator {
    memory: f64,
}

impl Calculator {
    pub fn new() -> Self {
        Calculator { memory: 0.0 }
    }

    pub fn add(&mut self, a: f64, b: f64) -> f64 {
        // 実装
    }

    pub fn subtract(&mut self, a: f64, b: f64) -> f64 {
        // 実装
    }

    pub fn multiply(&mut self, a: f64, b: f64) -> f64 {
        // 実装
    }

    pub fn divide(&mut self, a: f64, b: f64) -> Result<f64, String> {
        // 実装(0除算はエラー)
    }

    pub fn store(&mut self, value: f64) {
        // メモリに保存
    }

    pub fn recall(&self) -> f64 {
        // メモリから読み込み
    }

    pub fn clear_memory(&mut self) {
        // メモリをクリア
    }
}

2.2 単体テスト(10点)

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_addition() {
        // テストを実装
    }

    #[test]
    fn test_subtraction() {
        // テストを実装
    }

    #[test]
    fn test_multiplication() {
        // テストを実装
    }

    #[test]
    fn test_division() {
        // テストを実装
    }

    #[test]
    #[should_panic(expected = "0で除算")]
    fn test_division_by_zero() {
        // テストを実装
    }

    #[test]
    fn test_memory_operations() {
        // store, recall, clear_memoryのテスト
    }
}

要件

  • すべての公開関数に対するテスト
  • 正常系と異常系の両方をカバー
  • エッジケースのテスト(0、負の数、大きな数)

2.3 ドキュメントテスト(10点)

各関数にドキュメントコメントとテストを追加しなさい。

/// 2つの数を加算します。
///
/// # Examples
///
/// 
/// use calculator::Calculator; /// /// let mut calc = Calculator::new(); /// assert_eq!(calc.add(2.0, 3.0), 5.0); ///
pub fn add(&mut self, a: f64, b: f64) -> f64 {
    // 実装
}

要件

  • すべての公開関数にドキュメント
  • # Examplesセクション
  • # Panicsセクション(該当する場合)
  • # Errorsセクション(該当する場合)

問題3:統合テスト(20点)

実用的なシナリオをテストする統合テストを作成しなさい。

// tests/integration_tests.rs

use calculator::Calculator;

#[test]
fn test_complex_calculation() {
    // (10 + 5) * 2 / 3 = 10
    let mut calc = Calculator::new();

    let result1 = calc.add(10.0, 5.0);
    let result2 = calc.multiply(result1, 2.0);
    let result3 = calc.divide(result2, 3.0).unwrap();

    assert!((result3 - 10.0).abs() < 1e-10);
}

#[test]
fn test_memory_workflow() {
    // メモリ機能を使った実用的なシナリオ
}

#[test]
fn test_error_recovery() {
    // エラーが発生した後も正常に動作することを確認
}

要件

  • 最低5つの統合テスト
  • 実際の使用シナリオを反映
  • エラーハンドリングのテスト

問題4:TDDの実践(15点)

テスト駆動開発で以下の機能を追加しなさい。

4.1 平方根機能

手順

  • テストを先に書く(Red)
  • 実装する(Green)
  • リファクタリング(Refactor)

// 1. Red - テストを先に書く
#[test]
fn test_sqrt() {
    let mut calc = Calculator::new();
    assert_eq!(calc.sqrt(4.0).unwrap(), 2.0);
    assert_eq!(calc.sqrt(9.0).unwrap(), 3.0);
    assert!(calc.sqrt(-1.0).is_err());  // 負の数はエラー
}

// 2. Green - 実装
impl Calculator {
    pub fn sqrt(&self, x: f64) -> Result<f64, String> {
        // 実装
    }
}

// 3. Refactor - 必要に応じて改善

提出物

  • コミット履歴で各ステップを示す
  • git log --onelineの出力を含める

---

ボーナス課題

ボーナス1:プロパティベーステスト(10点)

quickcheckまたはproptestを使ったプロパティベーステストを実装しなさい。

# Cargo.toml
[dev-dependencies]
proptest = "1.0"

use proptest::prelude::*;

proptest! {
    #[test]
    fn test_addition_commutative(a in -1000.0..1000.0, b in -1000.0..1000.0) {
        let mut calc = Calculator::new();
        let result1 = calc.add(a, b);
        let result2 = calc.add(b, a);

        // 加算は可換である
        prop_assert!((result1 - result2).abs() < 1e-10);
    }

    #[test]
    fn test_multiplication_associative(
        a in -100.0..100.0,
        b in -100.0..100.0,
        c in -100.0..100.0
    ) {
        // (a * b) * c == a * (b * c)
        // 実装
    }
}

要件

  • 最低5つのプロパティテスト
  • 数学的性質の検証(可換性、結合性、分配性など)
  • エッジケースの自動発見

ボーナス2:モックとスタブ(10点)

依存性を持つコードのテストを実装しなさい。

// src/calculator_service.rs

pub trait Logger {
    fn log(&self, message: &str);
}

pub struct CalculatorService<L: Logger> {
    calculator: Calculator,
    logger: L,
}

impl<L: Logger> CalculatorService<L> {
    pub fn new(logger: L) -> Self {
        CalculatorService {
            calculator: Calculator::new(),
            logger,
        }
    }

    pub fn add_with_log(&mut self, a: f64, b: f64) -> f64 {
        self.logger.log(&format!("Adding {} + {}", a, b));
        let result = self.calculator.add(a, b);
        self.logger.log(&format!("Result: {}", result));
        result
    }
}

// テスト用モック
#[cfg(test)]
mod tests {
    use super::*;

    struct MockLogger {
        messages: std::cell::RefCell<Vec<String>>,
    }

    impl MockLogger {
        fn new() -> Self {
            MockLogger {
                messages: std::cell::RefCell::new(Vec::new()),
            }
        }

        fn get_messages(&self) -> Vec<String> {
            self.messages.borrow().clone()
        }
    }

    impl Logger for MockLogger {
        fn log(&self, message: &str) {
            self.messages.borrow_mut().push(message.to_string());
        }
    }

    #[test]
    fn test_logging() {
        let logger = MockLogger::new();
        let mut service = CalculatorService::new(logger);

        service.add_with_log(2.0, 3.0);

        let messages = service.logger.get_messages();
        assert_eq!(messages.len(), 2);
        assert!(messages[0].contains("Adding"));
        assert!(messages[1].contains("Result"));
    }
}

要件

  • トレイトを使った抽象化
  • テスト用のモック実装
  • モックを使った振る舞いの検証

ボーナス3:カバレッジ分析(10点)

テストカバレッジを測定し、100%を目指しなさい。

# Tarpaulinのインストール
cargo install cargo-tarpaulin

# カバレッジ測定
cargo tarpaulin --out Html --out Lcov

# LLVMカバレッジ
RUSTFLAGS="-C instrument-coverage" cargo test
llvm-profdata merge -sparse default.profraw -o coverage.profdata
llvm-cov show target/debug/calculator -instr-profile=coverage.profdata

提出物

  • カバレッジレポート(HTML)
  • カバレッジ率を示すスクリーンショット
  • 100%に到達できない場合は理由を説明

ボーナス4:ベンチマーク(10点)

Criterionを使ったベンチマークテストを実装しなさい。

// benches/calculator_bench.rs

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use calculator::Calculator;

fn bench_operations(c: &mut Criterion) {
    c.bench_function("add", |b| {
        let mut calc = Calculator::new();
        b.iter(|| calc.add(black_box(10.0), black_box(20.0)));
    });

    c.bench_function("divide", |b| {
        let mut calc = Calculator::new();
        b.iter(|| calc.divide(black_box(100.0), black_box(5.0)));
    });

    c.bench_function("complex_calculation", |b| {
        b.iter(|| {
            let mut calc = Calculator::new();
            let r1 = calc.add(black_box(10.0), black_box(5.0));
            let r2 = calc.multiply(r1, black_box(2.0));
            calc.divide(r2, black_box(3.0))
        });
    });
}

criterion_group!(benches, bench_operations);
criterion_main!(benches);

要件

  • すべての公開関数のベンチマーク
  • 複雑な操作のベンチマーク
  • 結果の分析レポート
  • ---

    評価基準

    マンダトリー部分(80点)

    項目 配点 評価ポイント
    問題1:基礎理解 15点 テストの種類と使い方の理解
    問題2:電卓テスト 30点 包括的なテストスイート
    問題3:統合テスト 20点 実用的なシナリオのカバー
    問題4:TDD実践 15点 Red-Green-Refactorの実践

    ボーナス部分(20点)

    項目 配点 評価ポイント
    ボーナス1:プロパティテスト 10点 数学的性質の検証
    ボーナス2:モック 10点 依存性の分離
    ボーナス3:カバレッジ 10点 高いカバレッジ率
    ボーナス4:ベンチマーク 10点 性能測定

    : ボーナスは最大20点まで加算されます。

    ---

    提出方法

    ファイル構成

    rust-foundations-20/
    ├── Cargo.toml
    ├── src/
    │   ├── lib.rs              # Calculator実装
    │   └── calculator_service.rs  # ボーナス2用
    ├── tests/
    │   └── integration_tests.rs
    ├── benches/
    │   └── calculator_bench.rs
    ├── answers.md              # 問題1の回答
    ├── tdd_log.txt             # git log出力
    └── coverage/
        └── index.html          # カバレッジレポート
    

    テスト実行

    # すべてのテスト
    cargo test
    
    # 統合テストのみ
    cargo test --test integration_tests
    
    # ドキュメントテスト
    cargo test --doc
    
    # カバレッジ
    cargo tarpaulin
    
    # ベンチマーク
    cargo bench
    

    ---

    ヒント

    問題2のヒント

    浮動小数点数の比較

    fn assert_float_eq(a: f64, b: f64) {
        assert!((a - b).abs() < 1e-10, "期待: {}, 実際: {}", b, a);
    }
    
    #[test]
    fn test_division() {
        let mut calc = Calculator::new();
        let result = calc.divide(10.0, 3.0).unwrap();
        assert_float_eq(result, 3.333333333333333);
    }
    

    問題4のヒント

    TDDのコミット例

    git commit -m "Red: Add failing test for sqrt"
    git commit -m "Green: Implement sqrt (naive)"
    git commit -m "Refactor: Use f64::sqrt for better precision"
    

    ボーナス1のヒント

    プロパティの例

  • 可換性:a + b == b + a
  • 結合性:(a + b) + c == a + (b + c)
  • 単位元:a + 0 == a
  • 逆元:a - a == 0
  • ボーナス3のヒント

    カバレッジ向上のコツ

    // すべてのブランチをテスト
    pub fn abs(x: f64) -> f64 {
        if x < 0.0 {  // この分岐
            -x
        } else {      // とこの分岐の両方をテスト
            x
        }
    }
    
    #[test]
    fn test_abs_positive() {
        assert_eq!(abs(5.0), 5.0);
    }
    
    #[test]
    fn test_abs_negative() {
        assert_eq!(abs(-5.0), 5.0);  // 両方のブランチをカバー
    }
    

    ---

    学習の確認

    この課題を通じて、以下を理解できたか確認してください:

  • [ ] テストの書き方(単体、統合、ドキュメント)
  • [ ] アサーションマクロの使い分け
  • [ ] TDDのサイクル
  • [ ] テストの独立性
  • [ ] カバレッジの重要性
  • [ ] モックとスタブの使い方

次の章では、ドキュメンテーションとrustdocを学びます。