using Application.Dto.Statistics; using Service.Transaction; namespace WebApi.Test.Application; /// /// TransactionStatisticsApplication 单元测试 /// public class TransactionStatisticsApplicationTest : BaseApplicationTest { private readonly ITransactionStatisticsService _statisticsService; private readonly IConfigService _configService; private readonly ILogger _logger; private readonly TransactionStatisticsApplication _application; public TransactionStatisticsApplicationTest() { _statisticsService = Substitute.For(); _configService = Substitute.For(); _logger = CreateMockLogger(); _application = new TransactionStatisticsApplication(_statisticsService, _configService, _logger); } #region GetBalanceStatisticsAsync Tests [Fact] public async Task GetBalanceStatisticsAsync_有效数据_应返回累计余额统计() { // Arrange var year = 2026; var month = 2; var savingClassify = "储蓄"; var dailyStats = new Dictionary { { "2026-02-01", (2, 500m, 1000m, 0m) }, { "2026-02-02", (1, 200m, 0m, 0m) }, { "2026-02-03", (2, 300m, 2000m, 0m) } }; _configService.GetConfigByKeyAsync("SavingsCategories").Returns(savingClassify); _statisticsService.GetDailyStatisticsAsync(year, month, savingClassify).Returns(dailyStats); // Act var result = await _application.GetBalanceStatisticsAsync(year, month); // Assert result.Should().HaveCount(3); result[0].Day.Should().Be(1); result[0].CumulativeBalance.Should().Be(500m); // 1000 - 500 result[1].Day.Should().Be(2); result[1].CumulativeBalance.Should().Be(300m); // 500 + (0 - 200) result[2].Day.Should().Be(3); result[2].CumulativeBalance.Should().Be(2000m); // 300 + (2000 - 300) } [Fact] public async Task GetBalanceStatisticsAsync_无数据_应返回空列表() { // Arrange var year = 2026; var month = 2; _configService.GetConfigByKeyAsync("SavingsCategories").Returns("储蓄"); _statisticsService.GetDailyStatisticsAsync(year, month, "储蓄") .Returns(new Dictionary()); // Act var result = await _application.GetBalanceStatisticsAsync(year, month); // Assert result.Should().BeEmpty(); } #endregion #region GetDailyStatisticsAsync Tests [Fact] public async Task GetDailyStatisticsAsync_有效数据_应返回每日统计() { // Arrange var year = 2026; var month = 2; var dailyStats = new Dictionary { { "2026-02-10", (3, 500m, 1000m, 100m) }, { "2026-02-11", (5, 800m, 2000m, 200m) } }; _configService.GetConfigByKeyAsync("SavingsCategories").Returns("储蓄"); _statisticsService.GetDailyStatisticsAsync(year, month, "储蓄").Returns(dailyStats); // Act var result = await _application.GetDailyStatisticsAsync(year, month); // Assert result.Should().HaveCount(2); result.Should().Contain(s => s.Day == 10 && s.Income == 1000m && s.Expense == 500m && s.Count == 3 && s.Saving == 100m); result.Should().Contain(s => s.Day == 11 && s.Income == 2000m && s.Expense == 800m && s.Count == 5 && s.Saving == 200m); } #endregion #region GetWeeklyStatisticsAsync Tests [Fact] public async Task GetWeeklyStatisticsAsync_有效日期范围_应返回周统计() { // Arrange var startDate = new DateTime(2026, 2, 1); var endDate = new DateTime(2026, 2, 7); var weeklyStats = new Dictionary { { "2026-02-01", (2, 200m, 500m, 50m) }, { "2026-02-07", (3, 300m, 800m, 100m) } }; _configService.GetConfigByKeyAsync("SavingsCategories").Returns("储蓄"); _statisticsService.GetDailyStatisticsByRangeAsync(startDate, endDate, "储蓄").Returns(weeklyStats); // Act var result = await _application.GetWeeklyStatisticsAsync(startDate, endDate); // Assert result.Should().HaveCount(2); result.Should().Contain(s => s.Day == 1 && s.Income == 500m && s.Expense == 200m); result.Should().Contain(s => s.Day == 7 && s.Income == 800m && s.Expense == 300m); } #endregion #region GetRangeStatisticsAsync Tests [Fact] public async Task GetRangeStatisticsAsync_有效日期范围_应返回汇总统计() { // Arrange var startDate = new DateTime(2026, 2, 1); var endDate = new DateTime(2026, 2, 28); var rangeStats = new Dictionary { { "2026-02-01", (3, 500m, 1000m, 0m) }, { "2026-02-02", (4, 800m, 2000m, 0m) }, { "2026-02-03", (2, 300m, 0m, 0m) } }; _statisticsService.GetDailyStatisticsByRangeAsync(startDate, endDate, null).Returns(rangeStats); // Act var result = await _application.GetRangeStatisticsAsync(startDate, endDate); // Assert result.Year.Should().Be(2026); result.Month.Should().Be(2); result.TotalIncome.Should().Be(3000m); result.TotalExpense.Should().Be(1600m); result.Balance.Should().Be(1400m); result.TotalCount.Should().Be(9); result.ExpenseCount.Should().Be(3); result.IncomeCount.Should().Be(2); } #endregion #region GetMonthlyStatisticsAsync Tests [Fact] public async Task GetMonthlyStatisticsAsync_有效年月_应返回月度统计() { // Arrange var year = 2026; var month = 2; var monthlyStats = new MonthlyStatistics { Year = year, Month = month, TotalIncome = 5000m, TotalExpense = 3000m, Balance = 2000m, IncomeCount = 10, ExpenseCount = 15, TotalCount = 25 }; _statisticsService.GetMonthlyStatisticsAsync(year, month).Returns(monthlyStats); // Act var result = await _application.GetMonthlyStatisticsAsync(year, month); // Assert result.Should().NotBeNull(); result.Year.Should().Be(year); result.Month.Should().Be(month); result.TotalIncome.Should().Be(5000m); result.TotalExpense.Should().Be(3000m); result.Balance.Should().Be(2000m); } #endregion #region GetCategoryStatisticsAsync Tests [Fact] public async Task GetCategoryStatisticsAsync_有效参数_应返回分类统计() { // Arrange var year = 2026; var month = 2; var type = TransactionType.Expense; var categoryStats = new List { new() { Classify = "餐饮", Amount = 1000m, Count = 10 }, new() { Classify = "交通", Amount = 500m, Count = 5 } }; _statisticsService.GetCategoryStatisticsAsync(year, month, type).Returns(categoryStats); // Act var result = await _application.GetCategoryStatisticsAsync(year, month, type); // Assert result.Should().HaveCount(2); result.Should().Contain(s => s.Classify == "餐饮" && s.Amount == 1000m); result.Should().Contain(s => s.Classify == "交通" && s.Amount == 500m); } #endregion #region GetCategoryStatisticsByDateRangeAsync Tests [Fact] public async Task GetCategoryStatisticsByDateRangeAsync_有效日期字符串_应返回分类统计() { // Arrange var startDate = "2026-02-01"; var endDate = "2026-02-28"; var type = TransactionType.Expense; var categoryStats = new List { new() { Classify = "餐饮", Amount = 1500m, Count = 15 } }; _statisticsService.GetCategoryStatisticsByDateRangeAsync( DateTime.Parse(startDate), DateTime.Parse(endDate), type ).Returns(categoryStats); // Act var result = await _application.GetCategoryStatisticsByDateRangeAsync(startDate, endDate, type); // Assert result.Should().HaveCount(1); result[0].Classify.Should().Be("餐饮"); result[0].Amount.Should().Be(1500m); } [Fact] public async Task GetCategoryStatisticsByDateRangeAsync_无效日期格式_应抛出异常() { // Arrange var startDate = "invalid-date"; var endDate = "2026-02-28"; var type = TransactionType.Expense; // Act & Assert await Assert.ThrowsAsync(() => _application.GetCategoryStatisticsByDateRangeAsync(startDate, endDate, type)); } #endregion #region GetTrendStatisticsAsync Tests [Fact] public async Task GetTrendStatisticsAsync_有效参数_应返回趋势统计() { // Arrange var startYear = 2026; var startMonth = 1; var monthCount = 3; var trendStats = new List { new() { Year = 2026, Month = 1, Income = 5000m, Expense = 3000m }, new() { Year = 2026, Month = 2, Income = 6000m, Expense = 3500m }, new() { Year = 2026, Month = 3, Income = 5500m, Expense = 3200m } }; _statisticsService.GetTrendStatisticsAsync(startYear, startMonth, monthCount).Returns(trendStats); // Act var result = await _application.GetTrendStatisticsAsync(startYear, startMonth, monthCount); // Assert result.Should().HaveCount(3); result[0].Year.Should().Be(2026); result[0].Month.Should().Be(1); } #endregion #region GetReasonGroupsAsync Tests [Fact] public async Task GetReasonGroupsAsync_有效分页参数_应返回分组数据() { // Arrange var pageIndex = 1; var pageSize = 10; var reasonGroups = new List { new() { Reason = "餐饮", Count = 20, TotalAmount = 1500m }, new() { Reason = "交通", Count = 15, TotalAmount = 800m } }; var total = 50; _statisticsService.GetReasonGroupsAsync(pageIndex, pageSize).Returns((reasonGroups, total)); // Act var result = await _application.GetReasonGroupsAsync(pageIndex, pageSize); // Assert result.list.Should().HaveCount(2); result.total.Should().Be(50); result.list[0].Reason.Should().Be("餐饮"); } #endregion }