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
}