Files
EmailBill/WebApi.Test/Budget/BudgetTest.cs

241 lines
12 KiB
C#
Raw Normal View History

2026-01-22 11:06:52 +08:00
using Microsoft.Extensions.Logging;
2026-01-21 18:52:31 +08:00
using Common;
namespace WebApi.Test.Budget;
public class BudgetTest : BaseTest
{
private readonly IBudgetRepository _budgetRepository = Substitute.For<IBudgetRepository>();
private readonly IBudgetArchiveRepository _budgetArchiveRepository = Substitute.For<IBudgetArchiveRepository>();
private readonly ITransactionRecordRepository _transactionsRepository = Substitute.For<ITransactionRecordRepository>();
private readonly IOpenAiService _openAiService = Substitute.For<IOpenAiService>();
private readonly IMessageService _messageService = Substitute.For<IMessageService>();
private readonly ILogger<BudgetService> _logger = Substitute.For<ILogger<BudgetService>>();
private readonly IBudgetSavingsService _budgetSavingsService = Substitute.For<IBudgetSavingsService>();
private readonly IDateTimeProvider _dateTimeProvider = Substitute.For<IDateTimeProvider>();
private readonly BudgetService _service;
public BudgetTest()
{
_dateTimeProvider.Now.Returns(new DateTime(2024, 1, 15));
_service = new BudgetService(
_budgetRepository,
_budgetArchiveRepository,
_transactionsRepository,
_openAiService,
_messageService,
_logger,
_budgetSavingsService,
_dateTimeProvider
);
}
[Fact]
public async Task GetCategoryStats_月度_Test()
{
// Arrange
var referenceDate = new DateTime(2024, 1, 15);
var budgets = new List<BudgetRecord>
{
new() { Id = 1, Name = "吃喝", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" },
new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" }
};
_budgetRepository.GetAllAsync().Returns(budgets);
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(args =>
{
var b = (BudgetRecord)args[0];
return b.Name == "吃喝" ? 1200m : 300m;
});
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
.Returns(new Dictionary<DateTime, decimal>());
// Act
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
// Assert
2026-01-22 11:06:52 +08:00
result.Month.Limit.Should().Be(2500); // 吃喝2000 + 交通500
result.Month.Current.Should().Be(1500); // 吃喝1200 + 交通300
2026-01-21 18:52:31 +08:00
}
[Fact]
public async Task GetCategoryStats_月度_硬性收支_Test()
{
// Arrange
var referenceDate = new DateTime(2024, 1, 15);
var budgets = new List<BudgetRecord>
{
new() { Id = 1, Name = "房租", Limit = 3100, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, IsMandatoryExpense = true }
};
_budgetRepository.GetAllAsync().Returns(budgets);
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(0m); // 实际支出的金额为0
_dateTimeProvider.Now.Returns(referenceDate);
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
.Returns(new Dictionary<DateTime, decimal>());
// Act
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
// Assert
// 1月有31天15号经过了15天
// 3100 * 15 / 31 = 1500
result.Month.Limit.Should().Be(3100);
result.Month.Current.Should().Be(1500);
}
[Fact]
public async Task GetCategoryStats_年度_1月_Test()
{
// Arrange
var referenceDate = new DateTime(2024, 1, 15);
var budgets = new List<BudgetRecord>
{
new() { Id = 1, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year }
};
_budgetRepository.GetAllAsync().Returns(budgets);
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Is<DateTime>(d => d.Month == 1 && d.Day == 1), Arg.Is<DateTime>(d => d.Month == 12 && d.Day == 31))
.Returns(2000m);
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
.Returns(new Dictionary<DateTime, decimal>());
// Act
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
// Assert
// 月度统计中,年度预算被忽略 (Limit=0)
result.Month.Limit.Should().Be(0);
result.Month.Current.Should().Be(0);
// 年度统计中
result.Year.Limit.Should().Be(12000);
result.Year.Current.Should().Be(2000);
}
[Fact]
public async Task GetCategoryStats_年度_1月_硬性收支_Test()
{
// Arrange
var referenceDate = new DateTime(2024, 1, 1); // 元旦
var budgets = new List<BudgetRecord>
{
new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true }
};
_budgetRepository.GetAllAsync().Returns(budgets);
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()).Returns(0m);
_dateTimeProvider.Now.Returns(referenceDate);
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
.Returns(new Dictionary<DateTime, decimal>());
// Act
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
// Assert
// 2024是闰年366天。1月1号是第1天。
// 3660 * 1 / 366 = 10
result.Year.Limit.Should().Be(3660);
result.Year.Current.Should().Be(10);
}
[Fact]
public async Task GetCategoryStats_年度_3月_硬性收支_Test()
{
// Arrange
var referenceDate = new DateTime(2024, 3, 31); // 3月最后一天
var budgets = new List<BudgetRecord>
{
new() { Id = 1, Name = "年度固定支出", Limit = 3660, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, IsMandatoryExpense = true }
};
_budgetRepository.GetAllAsync().Returns(budgets);
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>()).Returns(0m);
_dateTimeProvider.Now.Returns(referenceDate);
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
.Returns(new Dictionary<DateTime, decimal>());
// Act
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
// Assert
// 2024是闰年。1月(31) + 2月(29) + 3月(31) = 91天
// 3660 * 91 / 366 = 910
result.Year.Limit.Should().Be(3660);
result.Year.Current.Should().Be(910);
}
2026-01-22 11:06:52 +08:00
[Fact]
public async Task GetCategoryStats_月度_发生年度收支_Test()
{
// Arrange
var referenceDate = new DateTime(2024, 1, 15);
// 设置预算:包含月度预算和年度预算
var budgets = new List<BudgetRecord>
{
// 月度预算:吃喝
new() { Id = 1, Name = "吃喝", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "餐饮,零食" },
// 月度预算:交通
new() { Id = 2, Name = "交通", Limit = 500, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "交通" },
// 年度预算:年度旅游(当前月度发生了相关支出)
new() { Id = 3, Name = "年度旅游", Limit = 12000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Year, SelectedCategories = "旅游,度假" },
// 年度预算:年度奖金(当前月度发生了相关收入)
new() { Id = 4, Name = "年度奖金", Limit = 50000, Category = BudgetCategory.Income, Type = BudgetPeriodType.Year, SelectedCategories = "奖金,年终奖" }
};
_budgetRepository.GetAllAsync().Returns(budgets);
// 设置月度预算的当前金额
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
.Returns(args =>
{
var b = (BudgetRecord)args[0];
return b.Name switch
{
"吃喝" => 1200m, // 月度预算已用1200元
"交通" => 300m, // 月度预算已用300元
"年度旅游" => 2000m, // 年度预算1月份已用2000元
"年度奖金" => 10000m, // 年度预算1月份已收10000元
_ => 0m
};
});
// 设置趋势统计数据为空(简化测试)
_transactionsRepository.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
.Returns(new Dictionary<DateTime, decimal>());
// Act - 测试支出统计
var expenseResult = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate);
// Act - 测试收入统计
var incomeResult = await _service.GetCategoryStatsAsync(BudgetCategory.Income, referenceDate);
// Assert - 支出月度统计:只包含月度预算
expenseResult.Month.Limit.Should().Be(2500); // 吃喝2000 + 交通500
expenseResult.Month.Current.Should().Be(1500); // 吃喝1200 + 交通300
expenseResult.Month.Count.Should().Be(2); // 只包含2个月度预算
expenseResult.Month.Rate.Should().Be(1500m / 2500m * 100); // 60%
// Assert - 支出年度统计:包含所有预算(月度+年度)
expenseResult.Year.Limit.Should().Be(12000 + (2500 * 12)); // 年度旅游12000 + 月度预算折算为年度(2500*12)
expenseResult.Year.Current.Should().Be(2000 + 1500); // 年度旅游2000 + 月度预算1500
expenseResult.Year.Count.Should().Be(3); // 包含3个预算2个月度+1个年度
// Assert - 收入月度统计只包含月度预算这里没有月度收入预算所以应该为0
incomeResult.Month.Limit.Should().Be(0); // 没有月度收入预算
incomeResult.Month.Current.Should().Be(0); // 没有月度收入预算
incomeResult.Month.Count.Should().Be(0); // 没有月度收入预算
// Assert - 收入年度统计:包含所有预算(只有年度收入预算)
incomeResult.Year.Limit.Should().Be(50000); // 年度奖金50000
incomeResult.Year.Current.Should().Be(10000); // 年度奖金已收10000
incomeResult.Year.Count.Should().Be(1); // 包含1个年度收入预算
}
2026-01-21 18:52:31 +08:00
}