テスト戦略
Tax Calculation Core は純粋関数ライブラリとして、以下の多層テスト戦略を採用する。
→ テストルール全体: .claude/rules/testing.md → Golden Test データ: ゴールデンテスト
Golden Tests(国税庁計算例との完全一致)
docs/design/testing/golden-test-examples.md に整理した国税庁の設例データと1:1で対応するテストを実装する。
csharp
[Fact]
public void ExecutePipeline_ShouldMatchNtaExample1_WhenGivenGoldenTestInput()
{
// Arrange: NTA設例1(令和7年分 年末調整のしかた p57-59)
var input = GoldenTestData.NtaExample1;
var parameters = TaxYearParameters.ForYear(2025);
// Act
var result = TaxCalculationPipeline.ExecutePipeline(input, parameters);
// Assert: 全ステップの期待値と完全一致
result.SalaryIncomeAfterDeduction.Should().Be(7_020_000m); // ②
result.IncomeAdjustmentDeduction.Should().Be(47_000m); // ③
result.AdjustedSalaryIncome.Should().Be(6_973_000m); // ④
result.TotalIncomeDeductions.Should().Be(4_786_102m); // ⑤
result.TaxableIncome.Should().Be(2_186_000m); // ⑥
result.AssessedTax.Should().Be(121_100m); // ⑦
result.YeaIncomeTax.Should().Be(44_600m); // ⑨
result.YeaAnnualTax.Should().Be(45_500m); // ⑩
result.OverUnderPayment.Should().Be(-155_200m); // ⑪ (還付)
}Property Tests(FsCheck による不変条件検証)
csharp
[Property]
public Property TaxAmount_ShouldBeNonNegative_WhenIncomeIsNonNegative()
{
return Prop.ForAll(
Arb.From<decimal>().Filter(income => income >= 0),
income =>
{
var result = TaxCalculationPipeline.ExecutePipeline(
CreateInput(grossSalary: income), R7Parameters);
return result.YeaAnnualTax >= 0;
});
}
[Property]
public Property Pipeline_ShouldBePureFunction_WhenGivenSameInput()
{
return Prop.ForAll(
Arb.From<TaxCalculationInput>(),
input =>
{
var result1 = TaxCalculationPipeline.ExecutePipeline(input, R7Parameters);
var result2 = TaxCalculationPipeline.ExecutePipeline(input, R7Parameters);
return result1 == result2; // record の値等価性
});
}
[Property]
public Property TotalDeductions_ShouldNotExceedTotalIncome()
{
return Prop.ForAll(
Arb.From<TaxCalculationInput>(),
input =>
{
var result = TaxCalculationPipeline.ExecutePipeline(input, R7Parameters);
return result.TotalIncomeDeductions <= result.AdjustedSalaryIncome
|| result.TaxableIncome == 0;
});
}不変条件の一覧:
| 不変条件 | 検証内容 |
|---|---|
| 所得 ≥ 0 → 税額 ≥ 0 | 年調年税額は常に0以上 |
| 同一入力 → 同一結果 | 純粋関数保証(参照透過性) |
| 控除額 ≤ 総所得 or 課税所得 = 0 | 控除合計が所得を超える場合は課税所得がゼロにフロアされる |
| 各ステップの端数処理が仕様通り | 切捨て/切上げの精度検証 |
Boundary Tests(税率区分境界・控除閾値境界)
csharp
[Theory]
[InlineData(1_949_000, 0.05, 0)] // 5%区間の上端付近
[InlineData(1_950_000, 0.05, 0)] // 5%区間の上端(以下なので含む)
[InlineData(1_951_000, 0.10, 97_500)] // 10%区間の下端
[InlineData(3_299_000, 0.10, 97_500)] // 10%区間の上端付近
[InlineData(3_300_000, 0.10, 97_500)] // 10%区間の上端(以下なので含む)
[InlineData(3_301_000, 0.20, 427_500)] // 20%区間の下端
public void CalculateAssessedTax_ShouldApplyCorrectBracket_WhenAtBoundary(
decimal taxableIncome, decimal expectedRate, decimal expectedDeduction)
{
var result = TaxCalculationPipeline.CalculateAssessedTax(
taxableIncome, R7Parameters.TaxRateTable);
var expected = taxableIncome * expectedRate - expectedDeduction;
result.Should().Be(expected);
}追加の境界テスト対象:
| 境界 | テストケース |
|---|---|
| 給与収入850万円 | 所得金額調整控除の適用/非適用境界 |
| 共通閾値 T(R7: 58万円) | 扶養親族・同一生計配偶者の該当/非該当境界 |
| 本人の合計所得金額900万円/950万円/1,000万円 | 配偶者控除の3段階逓減境界 |
| 基礎控除の各所得区分境界 | 132万円/336万円/489万円/655万円/2,350万円等 |
| 1月1日生まれ vs 1月2日生まれ | 前日満了ルールによる年齢判定境界 |
Year-Versioned Tests(年度パラメータ差し替え)
csharp
[Theory]
[InlineData(2024, 550_000)] // R6: 最低保障55万円
[InlineData(2025, 650_000)] // R7: 最低保障65万円
public void SalaryDeduction_ShouldUseCorrectMinimum_WhenYearChanges(
int taxYear, decimal expectedMinimum)
{
var parameters = TaxYearParameters.ForYear(taxYear);
var result = TaxCalculationPipeline.CalculateSalaryIncomeAfterDeductionAsync(
1_000_000m, parameters.SalaryDeductionTable);
// 給与収入100万円 − 最低保障額 = 給与所得控除後の金額
result.Should().Be(1_000_000m - expectedMinimum);
}年度間差異テストの対象:
| 年度変更 | テスト内容 |
|---|---|
| R6 → R7 | 給与所得控除の最低保障額(55万円→65万円) |
| R6 → R7 | 共通閾値 T(48万円→58万円) |
| R6 → R7 | 基礎控除テーブルの区分変更 |
| R7 → R7 | 施行日前後(11/30以前 vs 12/1以後)のパラメータ差異 |
| R7 → R8(予定) | 共通閾値 T(58万円→62万円予定) |