2026-02-10 17:49:19 +08:00
|
|
|
using Application.Dto.Statistics;
|
|
|
|
|
using Service.Transaction;
|
|
|
|
|
|
|
|
|
|
namespace WebApi.Test.Application;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// TransactionStatisticsApplication 单元测试
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class TransactionStatisticsApplicationTest : BaseApplicationTest
|
|
|
|
|
{
|
|
|
|
|
private readonly ITransactionStatisticsService _statisticsService;
|
|
|
|
|
private readonly IConfigService _configService;
|
|
|
|
|
private readonly TransactionStatisticsApplication _application;
|
|
|
|
|
|
|
|
|
|
public TransactionStatisticsApplicationTest()
|
|
|
|
|
{
|
|
|
|
|
_statisticsService = Substitute.For<ITransactionStatisticsService>();
|
|
|
|
|
_configService = Substitute.For<IConfigService>();
|
2026-02-11 13:00:01 +08:00
|
|
|
_application = new TransactionStatisticsApplication(_statisticsService, _configService);
|
2026-02-10 17:49:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region GetBalanceStatisticsAsync Tests
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetBalanceStatisticsAsync_有效数据_应返回累计余额统计()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var year = 2026;
|
|
|
|
|
var month = 2;
|
|
|
|
|
var savingClassify = "储蓄";
|
|
|
|
|
var dailyStats = new Dictionary<string, (int count, decimal expense, decimal income, decimal saving)>
|
|
|
|
|
{
|
|
|
|
|
{ "2026-02-01", (2, 500m, 1000m, 0m) },
|
|
|
|
|
{ "2026-02-02", (1, 200m, 0m, 0m) },
|
|
|
|
|
{ "2026-02-03", (2, 300m, 2000m, 0m) }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("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<string>("SavingsCategories").Returns("储蓄");
|
|
|
|
|
_statisticsService.GetDailyStatisticsAsync(year, month, "储蓄")
|
|
|
|
|
.Returns(new Dictionary<string, (int, decimal, decimal, decimal)>());
|
|
|
|
|
|
|
|
|
|
// 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<string, (int count, decimal expense, decimal income, decimal saving)>
|
|
|
|
|
{
|
|
|
|
|
{ "2026-02-10", (3, 500m, 1000m, 100m) },
|
|
|
|
|
{ "2026-02-11", (5, 800m, 2000m, 200m) }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("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<string, (int count, decimal expense, decimal income, decimal saving)>
|
|
|
|
|
{
|
|
|
|
|
{ "2026-02-01", (2, 200m, 500m, 50m) },
|
|
|
|
|
{ "2026-02-07", (3, 300m, 800m, 100m) }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_configService.GetConfigByKeyAsync<string>("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<string, (int count, decimal expense, decimal income, decimal saving)>
|
|
|
|
|
{
|
|
|
|
|
{ "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<CategoryStatistics>
|
|
|
|
|
{
|
|
|
|
|
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<CategoryStatistics>
|
|
|
|
|
{
|
|
|
|
|
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<FormatException>(() =>
|
|
|
|
|
_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<TrendStatistics>
|
|
|
|
|
{
|
|
|
|
|
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<ReasonGroupDto>
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
}
|