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
280 lines
11 KiB
C#
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);
|
|
}
|
|
}
|