using Service.Transaction; namespace WebApi.Test.Transaction; public class TransactionStatisticsServiceTest : BaseTest { private readonly ITransactionRecordRepository _transactionRepository = Substitute.For(); private readonly ITransactionStatisticsService _service; public TransactionStatisticsServiceTest() { // 默认配置 QueryAsync 返回空列表 _transactionRepository.QueryAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ReturnsForAnyArgs(new List()); _service = new TransactionStatisticsService( _transactionRepository ); } private void ConfigureQueryAsync(List data) { _transactionRepository.QueryAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .ReturnsForAnyArgs(data); } [Fact] public async Task GetDailyStatisticsAsync_基本测试() { // Arrange var year = 2024; var month = 1; var testData = new List { 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 GetTrendStatisticsAsync_多个月份() { // Arrange var startYear = 2024; var startMonth = 1; var monthCount = 3; var allRecords = new List { // 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(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() ).Returns(callInfo => { var y = callInfo.ArgAt(0); var m = callInfo.ArgAt(1); var type = callInfo.ArgAt(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 { 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 { "麦当劳" }; var testData = new List { new() { Id=1, Reason="麦当劳午餐", Classify="餐饮" } }; // Needs to mock GetClassifiedByKeywordsAsync _transactionRepository.GetClassifiedByKeywordsAsync(Arg.Any>(), Arg.Any()) .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 { 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 { 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(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() ).Returns(callInfo => { var type = callInfo.ArgAt(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); } }