Files
EmailBill/WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs
SunCheng 61916dc6da
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 31s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
fix
2026-02-01 10:27:04 +08:00

280 lines
11 KiB
C#

using Service.Transaction;
namespace WebApi.Test.Transaction;
public class TransactionStatisticsServiceTest : BaseTest
{
private readonly ITransactionRecordRepository _transactionRepository = Substitute.For<ITransactionRecordRepository>();
private readonly ITransactionStatisticsService _service;
public TransactionStatisticsServiceTest()
{
// 默认配置 QueryAsync 返回空列表
_transactionRepository.QueryAsync(
Arg.Any<int?>(),
Arg.Any<int?>(),
Arg.Any<DateTime?>(),
Arg.Any<DateTime?>(),
Arg.Any<TransactionType?>(),
Arg.Any<string[]>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<int>(),
Arg.Any<int>(),
Arg.Any<bool>())
.ReturnsForAnyArgs(new List<TransactionRecord>());
_service = new TransactionStatisticsService(
_transactionRepository
);
}
private void ConfigureQueryAsync(List<TransactionRecord> data)
{
_transactionRepository.QueryAsync(
Arg.Any<int?>(),
Arg.Any<int?>(),
Arg.Any<DateTime?>(),
Arg.Any<DateTime?>(),
Arg.Any<TransactionType?>(),
Arg.Any<string[]>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<int>(),
Arg.Any<int>(),
Arg.Any<bool>())
.ReturnsForAnyArgs(data);
}
[Fact]
public async Task GetDailyStatisticsAsync_基本测试()
{
// Arrange
var year = 2024;
var month = 1;
var testData = new List<TransactionRecord>
{
new() { Id=1, OccurredAt=new DateTime(2024,1,1), Amount=-100m, Type=TransactionType.Expense },
new() { Id=2, OccurredAt=new DateTime(2024,1,1), Amount=-50m, Type=TransactionType.Expense },
new() { Id=3, OccurredAt=new DateTime(2024,1,2), Amount=5000m, Type=TransactionType.Income }
};
ConfigureQueryAsync(testData);
// Act
var result = await _service.GetDailyStatisticsAsync(year, month);
// Assert
result.Should().ContainKey("2024-01-01");
result["2024-01-01"].expense.Should().Be(150m);
}
[Fact]
public async Task GetDailyStatisticsAsync_月份为0查询全年()
{
// Arrange
var year = 2024;
var month = 0; // 0 表示查询全年
var testData = new List<TransactionRecord>
{
// 1月
new() { Id=1, OccurredAt=new DateTime(2024,1,15), Amount=-100m, Type=TransactionType.Expense },
new() { Id=2, OccurredAt=new DateTime(2024,1,20), Amount=5000m, Type=TransactionType.Income },
// 6月
new() { Id=3, OccurredAt=new DateTime(2024,6,10), Amount=-200m, Type=TransactionType.Expense },
new() { Id=4, OccurredAt=new DateTime(2024,6,15), Amount=3000m, Type=TransactionType.Income },
// 12月
new() { Id=5, OccurredAt=new DateTime(2024,12,25), Amount=-300m, Type=TransactionType.Expense },
new() { Id=6, OccurredAt=new DateTime(2024,12,31), Amount=2000m, Type=TransactionType.Income }
};
ConfigureQueryAsync(testData);
// Act
var result = await _service.GetDailyStatisticsAsync(year, month);
// Assert - 应包含全年各个月份的数据
result.Should().ContainKey("2024-01-15");
result.Should().ContainKey("2024-06-10");
result.Should().ContainKey("2024-12-31");
result["2024-01-15"].expense.Should().Be(100m);
result["2024-06-10"].expense.Should().Be(200m);
result["2024-12-31"].income.Should().Be(2000m);
}
[Fact]
public async Task GetDailyStatisticsAsync_月份为0不应抛出异常()
{
// Arrange
var year = 2026;
var month = 0;
ConfigureQueryAsync(new List<TransactionRecord>());
// Act & Assert - 不应抛出 ArgumentOutOfRangeException
var act = async () => await _service.GetDailyStatisticsAsync(year, month);
await act.Should().NotThrowAsync<ArgumentOutOfRangeException>();
}
[Fact]
public async Task GetDailyStatisticsAsync_包含存款分类统计()
{
// Arrange
var year = 2024;
var month = 1;
var savingClassify = "股票,基金"; // 存款分类
var testData = new List<TransactionRecord>
{
new() { Id=1, OccurredAt=new DateTime(2024,1,1), Amount=-100m, Type=TransactionType.Expense, Classify="餐饮" },
new() { Id=2, OccurredAt=new DateTime(2024,1,1), Amount=-500m, Type=TransactionType.Expense, Classify="股票" },
new() { Id=3, OccurredAt=new DateTime(2024,1,1), Amount=-300m, Type=TransactionType.Expense, Classify="基金" }
};
ConfigureQueryAsync(testData);
// Act
var result = await _service.GetDailyStatisticsAsync(year, month, savingClassify);
// Assert
result.Should().ContainKey("2024-01-01");
result["2024-01-01"].saving.Should().Be(800m); // 股票500 + 基金300
result["2024-01-01"].expense.Should().Be(900m); // 总支出
}
[Fact]
public async Task GetTrendStatisticsAsync_多个月份()
{
// Arrange
var startYear = 2024;
var startMonth = 1;
var monthCount = 3;
var allRecords = new List<TransactionRecord>
{
// Month 1
new() { Id = 1, OccurredAt = new DateTime(2024, 1, 1), Amount = -1000m, Type = TransactionType.Expense },
new() { Id = 2, OccurredAt = new DateTime(2024, 1, 5), Amount = 5000m, Type = TransactionType.Income },
// Month 2
new() { Id = 3, OccurredAt = new DateTime(2024, 2, 1), Amount = -1500m, Type = TransactionType.Expense },
new() { Id = 4, OccurredAt = new DateTime(2024, 2, 5), Amount = 5000m, Type = TransactionType.Income },
// Month 3
new() { Id = 5, OccurredAt = new DateTime(2024, 3, 1), Amount = -2000m, Type = TransactionType.Expense },
new() { Id = 6, OccurredAt = new DateTime(2024, 3, 5), Amount = 5000m, Type = TransactionType.Income }
};
// Mock Logic: filter by year (Arg[0]) and month (Arg[1]) and type (Arg[4]) if provided
_transactionRepository.QueryAsync(
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
).Returns(callInfo =>
{
var y = callInfo.ArgAt<int?>(0);
var m = callInfo.ArgAt<int?>(1);
var type = callInfo.ArgAt<TransactionType?>(4);
var query = allRecords.AsEnumerable();
if (y.HasValue) query = query.Where(t => t.OccurredAt.Year == y.Value);
if (m.HasValue) query = query.Where(t => t.OccurredAt.Month == m.Value);
// Service calls QueryAsync with 'type' parameter?
// In GetTrendStatisticsAsync: transactionRepository.QueryAsync(year: targetYear, month: targetMonth...)
// It does NOT pass type. So type is null.
// But Service THEN filters by Type in memory.
return query.ToList();
});
// Act
var result = await _service.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
// Assert
result.Should().HaveCount(3);
result[0].Month.Should().Be(1);
result[0].Expense.Should().Be(1000m); // Abs(-1000)
}
[Fact]
public async Task GetReasonGroupsAsync_基本测试()
{
// Arrange
var testData = new List<TransactionRecord>
{
new() { Id=1, Reason="M", Classify="", Amount=-50m, Type=TransactionType.Expense },
new() { Id=2, Reason="M", Classify="", Amount=-80m, Type=TransactionType.Expense }
};
ConfigureQueryAsync(testData);
// Act
var result = await _service.GetReasonGroupsAsync();
// Assert
var item = result.list.First(x => x.Reason == "M");
item.TotalAmount.Should().Be(130m); // Expect positive (Abs) as per Service logic
}
[Fact]
public async Task GetClassifiedByKeywordsWithScoreAsync_基本测试()
{
// Arrange
var keywords = new List<string> { "麦当劳" };
var testData = new List<TransactionRecord>
{
new() { Id=1, Reason="麦当劳午餐", Classify="餐饮" }
};
// Needs to mock GetClassifiedByKeywordsAsync
_transactionRepository.GetClassifiedByKeywordsAsync(Arg.Any<List<string>>(), Arg.Any<int>())
.Returns(Task.FromResult(testData));
// Act
var result = await _service.GetClassifiedByKeywordsWithScoreAsync(keywords);
// Assert
result.Should().HaveCount(1);
result[0].record.Reason.Should().Contain("麦当劳");
}
[Fact]
public async Task GetAmountGroupByClassifyAsync_基本测试()
{
// Arrange
var testData = new List<TransactionRecord>
{
new() { Amount=-100m, Type=TransactionType.Expense, Classify="餐饮" },
new() { Amount=-50m, Type=TransactionType.Expense, Classify="餐饮" }
};
ConfigureQueryAsync(testData);
// Act
var result = await _service.GetAmountGroupByClassifyAsync(DateTime.Now, DateTime.Now);
// Assert
result[("餐饮", TransactionType.Expense)].Should().Be(-150m); // Expect Negative (Sum of amounts)
}
// Additional tests from original file to maintain coverage, with minimal adjustments if needed
[Fact]
public async Task GetCategoryStatisticsAsync_支出分类()
{
var year = 2024; var month = 1;
var testData = new List<TransactionRecord>
{
new() { Amount = -100m, Type = TransactionType.Expense, Classify = "餐饮" },
new() { Amount = -50m, Type = TransactionType.Expense, Classify = "餐饮" },
new() { Amount = -200m, Type = TransactionType.Expense, Classify = "交通" }
};
// Mock filtering by Type
_transactionRepository.QueryAsync(
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
).Returns(callInfo =>
{
var type = callInfo.ArgAt<TransactionType?>(4);
return testData.Where(t => !type.HasValue || t.Type == type).ToList();
});
var result = await _service.GetCategoryStatisticsAsync(year, month, TransactionType.Expense);
result.First(c => c.Classify == "餐饮").Amount.Should().Be(150m);
result.First(c => c.Classify == "交通").Amount.Should().Be(200m);
}
}