测试覆盖率
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 27s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 27s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
608
WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs
Normal file
608
WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs
Normal file
@@ -0,0 +1,608 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Service.Transaction;
|
||||
|
||||
namespace WebApi.Test.Transaction;
|
||||
|
||||
public class TransactionPeriodicServiceTest : BaseTest
|
||||
{
|
||||
private readonly ITransactionPeriodicRepository _periodicRepository = Substitute.For<ITransactionPeriodicRepository>();
|
||||
private readonly ITransactionRecordRepository _transactionRepository = Substitute.For<ITransactionRecordRepository>();
|
||||
private readonly IMessageRecordRepository _messageRepository = Substitute.For<IMessageRecordRepository>();
|
||||
private readonly ILogger<TransactionPeriodicService> _logger = Substitute.For<ILogger<TransactionPeriodicService>>();
|
||||
private readonly ITransactionPeriodicService _service;
|
||||
|
||||
public TransactionPeriodicServiceTest()
|
||||
{
|
||||
_service = new TransactionPeriodicService(
|
||||
_periodicRepository,
|
||||
_transactionRepository,
|
||||
_messageRepository,
|
||||
_logger
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_每日账单()
|
||||
{
|
||||
// Arrange
|
||||
var today = new DateTime(2024, 1, 15, 10, 0, 0);
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
||||
t.Amount == -100m &&
|
||||
t.Type == TransactionType.Expense &&
|
||||
t.Classify == "餐饮" &&
|
||||
t.Reason == "每日餐费" &&
|
||||
t.Card == "周期性账单" &&
|
||||
t.ImportFrom == "周期性账单自动生成"
|
||||
));
|
||||
|
||||
await _messageRepository.Received(1).AddAsync(Arg.Is<MessageRecord>(m =>
|
||||
m.Title == "周期性账单提醒" &&
|
||||
m.Content.Contains("支出") &&
|
||||
m.Content.Contains("100.00") &&
|
||||
m.Content.Contains("每日餐费") &&
|
||||
m.IsRead == false
|
||||
));
|
||||
|
||||
await _periodicRepository.Received(1).UpdateExecuteTimeAsync(
|
||||
Arg.Is(1L),
|
||||
Arg.Is<DateTime>(dt => dt.Date == today.Date),
|
||||
Arg.Is<DateTime?>(dt => dt.HasValue && dt.Value.Date == today.Date.AddDays(1))
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_每周账单()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Weekly,
|
||||
PeriodicConfig = "1,3,5", // 周一、三、五
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "每周通勤费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 10, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
||||
t.Amount == -200m &&
|
||||
t.Type == TransactionType.Expense &&
|
||||
t.Classify == "交通" &&
|
||||
t.Reason == "每周通勤费"
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_每月账单()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Monthly,
|
||||
PeriodicConfig = "1,15", // 每月1号和15号
|
||||
Amount = 5000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "工资",
|
||||
Reason = "每月工资",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 1, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t =>
|
||||
t.Amount == 5000m &&
|
||||
t.Type == TransactionType.Income &&
|
||||
t.Classify == "工资" &&
|
||||
t.Reason == "每月工资"
|
||||
));
|
||||
|
||||
await _messageRepository.Received(1).AddAsync(Arg.Is<MessageRecord>(m =>
|
||||
m.Content.Contains("收入") &&
|
||||
m.Content.Contains("5000.00") &&
|
||||
m.Content.Contains("每月工资")
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_未达到执行时间()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Weekly,
|
||||
PeriodicConfig = "1,3,5", // 只在周一(1)、三(3)、五(5)执行
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "每周通勤费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 10, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _transactionRepository.Received(0).AddAsync(Arg.Any<TransactionRecord>());
|
||||
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
|
||||
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_今天已执行过()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 15, 8, 0, 0), // 今天已经执行过
|
||||
NextExecuteTime = new DateTime(2024, 1, 16, 10, 0, 0)
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
// 由于 LastExecuteTime 日期是今天,所以不会再次执行
|
||||
await _transactionRepository.Received(0).AddAsync(Arg.Any<TransactionRecord>());
|
||||
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
|
||||
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_从未执行过()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Weekly,
|
||||
PeriodicConfig = "1,3,5",
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "每周通勤费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = null, // 从未执行过
|
||||
NextExecuteTime = null
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _transactionRepository.Received(1).AddAsync(Arg.Any<TransactionRecord>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_添加交易记录失败()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBill = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(new[] { periodicBill });
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(false); // 添加失败
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _messageRepository.Received(0).AddAsync(Arg.Any<MessageRecord>());
|
||||
await _periodicRepository.Received(0).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_多条账单()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBills = new[]
|
||||
{
|
||||
new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
},
|
||||
new TransactionPeriodic
|
||||
{
|
||||
Id = 2,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "每日交通",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
await _transactionRepository.Received(2).AddAsync(Arg.Any<TransactionRecord>());
|
||||
await _messageRepository.Received(2).AddAsync(Arg.Any<MessageRecord>());
|
||||
await _periodicRepository.Received(2).UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每日()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
PeriodicConfig = ""
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 1, 16, 0, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每周_本周内()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Weekly,
|
||||
PeriodicConfig = "1,3,5" // 周一、三、五
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0); // 周一
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 1, 17, 0, 0, 0)); // 下周三
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每周_跨周()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Weekly,
|
||||
PeriodicConfig = "1,3,5" // 周一、三、五
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 19, 10, 0, 0); // 周五
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 1, 22, 0, 0, 0)); // 下周一
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每月_本月内()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Monthly,
|
||||
PeriodicConfig = "1,15" // 每月1号和15号
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 10, 10, 0, 0);
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 1, 15, 0, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每月_跨月()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Monthly,
|
||||
PeriodicConfig = "1,15" // 每月1号和15号
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 16, 10, 0, 0);
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 2, 1, 0, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每月_月末()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Monthly,
|
||||
PeriodicConfig = "30,31" // 每月30号和31号
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0); // 1月只有31天
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 1, 30, 0, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每月_小月()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Monthly,
|
||||
PeriodicConfig = "30,31" // 每月30号和31号
|
||||
};
|
||||
var baseTime = new DateTime(2024, 4, 25, 10, 0, 0); // 4月只有30天
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 4, 30, 0, 0, 0)); // 30号,31号不存在
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每季度()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Quarterly,
|
||||
PeriodicConfig = "15" // 每季度第15天
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2024, 4, 15, 0, 0, 0)); // 下季度
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_每年()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Yearly,
|
||||
PeriodicConfig = "100" // 每年第100天
|
||||
};
|
||||
var baseTime = new DateTime(2024, 4, 10, 10, 0, 0); // 第100天是4月9日
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().Be(new DateTime(2025, 4, 9, 0, 0, 0)); // 下一年
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateNextExecuteTime_未知周期类型()
|
||||
{
|
||||
// Arrange
|
||||
var periodic = new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = (PeriodicType)99, // 未知类型
|
||||
PeriodicConfig = ""
|
||||
};
|
||||
var baseTime = new DateTime(2024, 1, 15, 10, 0, 0);
|
||||
|
||||
// Act
|
||||
var result = _service.CalculateNextExecuteTime(periodic, baseTime);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_处理异常不中断()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBills = new[]
|
||||
{
|
||||
new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
},
|
||||
new TransactionPeriodic
|
||||
{
|
||||
Id = 2,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "每日交通",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(args =>
|
||||
{
|
||||
var id = (long)args[0];
|
||||
if (id == 1)
|
||||
{
|
||||
throw new Exception("更新失败");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
// 第二条记录应该成功处理
|
||||
await _transactionRepository.Received(2).AddAsync(Arg.Any<TransactionRecord>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecutePeriodicBillsAsync_处理所有账单()
|
||||
{
|
||||
// Arrange
|
||||
var periodicBills = new[]
|
||||
{
|
||||
new TransactionPeriodic
|
||||
{
|
||||
Id = 1,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "每日餐费",
|
||||
IsEnabled = false, // 禁用
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
},
|
||||
new TransactionPeriodic
|
||||
{
|
||||
Id = 2,
|
||||
PeriodicType = PeriodicType.Daily,
|
||||
Amount = 200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "每日交通",
|
||||
IsEnabled = true,
|
||||
LastExecuteTime = new DateTime(2024, 1, 14, 10, 0, 0),
|
||||
NextExecuteTime = new DateTime(2024, 1, 15, 10, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
_periodicRepository.GetPendingPeriodicBillsAsync().Returns(periodicBills);
|
||||
_transactionRepository.AddAsync(Arg.Any<TransactionRecord>()).Returns(Task.FromResult(true));
|
||||
_messageRepository.AddAsync(Arg.Any<MessageRecord>()).Returns(Task.CompletedTask);
|
||||
_periodicRepository.UpdateExecuteTimeAsync(Arg.Any<long>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await _service.ExecutePeriodicBillsAsync();
|
||||
|
||||
// Assert
|
||||
// 只有启用的账单会被处理
|
||||
await _transactionRepository.Received(1).AddAsync(Arg.Is<TransactionRecord>(t => t.Reason == "每日交通"));
|
||||
await _transactionRepository.Received(0).AddAsync(Arg.Is<TransactionRecord>(t => t.Reason == "每日餐费"));
|
||||
}
|
||||
}
|
||||
972
WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs
Normal file
972
WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs
Normal file
@@ -0,0 +1,972 @@
|
||||
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, 10, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "午餐"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 15, 0, 0),
|
||||
Amount = -50m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "地铁"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 1, 2, 9, 0, 0),
|
||||
Amount = 5000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "工资",
|
||||
Reason = "工资收入"
|
||||
}
|
||||
};
|
||||
|
||||
ConfigureQueryAsync(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetDailyStatisticsAsync(year, month);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().ContainKey("2024-01-01");
|
||||
result.Should().ContainKey("2024-01-02");
|
||||
|
||||
result["2024-01-01"].count.Should().Be(2);
|
||||
result["2024-01-01"].expense.Should().Be(150m);
|
||||
result["2024-01-01"].income.Should().Be(0m);
|
||||
|
||||
result["2024-01-02"].count.Should().Be(1);
|
||||
result["2024-01-02"].expense.Should().Be(0m);
|
||||
result["2024-01-02"].income.Should().Be(5000m);
|
||||
}
|
||||
|
||||
[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, 10, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "午餐"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 15, 0, 0),
|
||||
Amount = -1000m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "投资",
|
||||
Reason = "基金定投"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 1, 2, 9, 0, 0),
|
||||
Amount = -500m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "存款",
|
||||
Reason = "银行存款"
|
||||
}
|
||||
};
|
||||
|
||||
_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(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetDailyStatisticsAsync(year, month, savingClassify);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
result["2024-01-01"].count.Should().Be(2);
|
||||
result["2024-01-01"].expense.Should().Be(1100m);
|
||||
result["2024-01-01"].income.Should().Be(0m);
|
||||
result["2024-01-01"].saving.Should().Be(1000m);
|
||||
|
||||
result["2024-01-02"].count.Should().Be(1);
|
||||
result["2024-01-02"].expense.Should().Be(500m);
|
||||
result["2024-01-02"].income.Should().Be(0m);
|
||||
result["2024-01-02"].saving.Should().Be(500m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDailyStatisticsByRangeAsync_基本测试()
|
||||
{
|
||||
// Arrange
|
||||
var startDate = new DateTime(2024, 1, 1);
|
||||
var endDate = new DateTime(2024, 1, 5);
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
OccurredAt = new DateTime(2024, 1, 3, 10, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "午餐"
|
||||
}
|
||||
};
|
||||
|
||||
_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(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetDailyStatisticsByRangeAsync(startDate, endDate);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
result.Should().ContainKey("2024-01-03");
|
||||
result["2024-01-03"].count.Should().Be(1);
|
||||
result["2024-01-03"].expense.Should().Be(100m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMonthlyStatisticsAsync_基本测试()
|
||||
{
|
||||
// Arrange
|
||||
var year = 2024;
|
||||
var month = 1;
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 10, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "午餐"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 1, 2, 15, 0, 0),
|
||||
Amount = -50m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "地铁"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 1, 5, 9, 0, 0),
|
||||
Amount = 5000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "工资",
|
||||
Reason = "工资收入"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 4,
|
||||
OccurredAt = new DateTime(2024, 1, 10, 9, 0, 0),
|
||||
Amount = 2000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "奖金",
|
||||
Reason = "奖金收入"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
year,
|
||||
month,
|
||||
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(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetMonthlyStatisticsAsync(year, month);
|
||||
|
||||
// Assert
|
||||
result.Year.Should().Be(year);
|
||||
result.Month.Should().Be(month);
|
||||
result.TotalExpense.Should().Be(150m);
|
||||
result.TotalIncome.Should().Be(7000m);
|
||||
result.Balance.Should().Be(6850m);
|
||||
result.ExpenseCount.Should().Be(2);
|
||||
result.IncomeCount.Should().Be(2);
|
||||
result.TotalCount.Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMonthlyStatisticsAsync_无数据()
|
||||
{
|
||||
// Arrange
|
||||
var year = 2024;
|
||||
var month = 2;
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
year,
|
||||
month,
|
||||
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(new List<TransactionRecord>());
|
||||
|
||||
// Act
|
||||
var result = await _service.GetMonthlyStatisticsAsync(year, month);
|
||||
|
||||
// Assert
|
||||
result.Year.Should().Be(year);
|
||||
result.Month.Should().Be(month);
|
||||
result.TotalExpense.Should().Be(0m);
|
||||
result.TotalIncome.Should().Be(0m);
|
||||
result.Balance.Should().Be(0m);
|
||||
result.ExpenseCount.Should().Be(0);
|
||||
result.IncomeCount.Should().Be(0);
|
||||
result.TotalCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStatisticsAsync_支出分类()
|
||||
{
|
||||
// Arrange
|
||||
var year = 2024;
|
||||
var month = 1;
|
||||
var type = TransactionType.Expense;
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 10, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "午餐"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 1, 2, 15, 0, 0),
|
||||
Amount = -50m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "晚餐"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 1, 3, 9, 0, 0),
|
||||
Amount = -200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通",
|
||||
Reason = "打车"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 4,
|
||||
OccurredAt = new DateTime(2024, 1, 5, 9, 0, 0),
|
||||
Amount = 5000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "工资",
|
||||
Reason = "工资收入"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
year,
|
||||
month,
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<DateTime>(),
|
||||
type,
|
||||
Arg.Any<string[]>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<bool>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatisticsAsync(year, month, type);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
var dining = result.First(c => c.Classify == "餐饮");
|
||||
dining.Amount.Should().Be(150m);
|
||||
dining.Count.Should().Be(2);
|
||||
dining.Percent.Should().Be(42.9m);
|
||||
|
||||
var transport = result.First(c => c.Classify == "交通");
|
||||
transport.Amount.Should().Be(200m);
|
||||
transport.Count.Should().Be(1);
|
||||
transport.Percent.Should().Be(57.1m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCategoryStatisticsAsync_收入分类()
|
||||
{
|
||||
// Arrange
|
||||
var year = 2024;
|
||||
var month = 1;
|
||||
var type = TransactionType.Income;
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 10, 0, 0),
|
||||
Amount = 5000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "工资",
|
||||
Reason = "工资收入"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 1, 2, 15, 0, 0),
|
||||
Amount = 1000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "奖金",
|
||||
Reason = "绩效奖金"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 1, 3, 9, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮",
|
||||
Reason = "午餐"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
year,
|
||||
month,
|
||||
Arg.Any<DateTime>(),
|
||||
Arg.Any<DateTime>(),
|
||||
type,
|
||||
Arg.Any<string[]>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<bool>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetCategoryStatisticsAsync(year, month, type);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
var salary = result.First(c => c.Classify == "工资");
|
||||
salary.Amount.Should().Be(5000m);
|
||||
salary.Count.Should().Be(1);
|
||||
salary.Percent.Should().Be(83.3m);
|
||||
|
||||
var bonus = result.First(c => c.Classify == "奖金");
|
||||
bonus.Amount.Should().Be(1000m);
|
||||
bonus.Count.Should().Be(1);
|
||||
bonus.Percent.Should().Be(16.7m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTrendStatisticsAsync_多个月份()
|
||||
{
|
||||
// Arrange
|
||||
var startYear = 2024;
|
||||
var startMonth = 1;
|
||||
var monthCount = 3;
|
||||
|
||||
var mockData = new Dictionary<int, List<TransactionRecord>>
|
||||
{
|
||||
[1] = new List<TransactionRecord>
|
||||
{
|
||||
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 }
|
||||
},
|
||||
[2] = new List<TransactionRecord>
|
||||
{
|
||||
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 }
|
||||
},
|
||||
[3] = new List<TransactionRecord>
|
||||
{
|
||||
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 }
|
||||
}
|
||||
};
|
||||
|
||||
_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(args =>
|
||||
{
|
||||
var month = (int)args[1];
|
||||
if (mockData.ContainsKey(month))
|
||||
{
|
||||
return mockData[month];
|
||||
}
|
||||
return new List<TransactionRecord>();
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await _service.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
|
||||
result[0].Year.Should().Be(2024);
|
||||
result[0].Month.Should().Be(1);
|
||||
result[0].Expense.Should().Be(1000m);
|
||||
result[0].Income.Should().Be(5000m);
|
||||
result[0].Balance.Should().Be(4000m);
|
||||
|
||||
result[1].Year.Should().Be(2024);
|
||||
result[1].Month.Should().Be(2);
|
||||
result[1].Expense.Should().Be(1500m);
|
||||
result[1].Income.Should().Be(5000m);
|
||||
result[1].Balance.Should().Be(3500m);
|
||||
|
||||
result[2].Year.Should().Be(2024);
|
||||
result[2].Month.Should().Be(3);
|
||||
result[2].Expense.Should().Be(2000m);
|
||||
result[2].Income.Should().Be(5000m);
|
||||
result[2].Balance.Should().Be(3000m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTrendStatisticsAsync_跨年()
|
||||
{
|
||||
// Arrange
|
||||
var startYear = 2024;
|
||||
var startMonth = 11;
|
||||
var monthCount = 4;
|
||||
|
||||
_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(new List<TransactionRecord>());
|
||||
|
||||
// Act
|
||||
var result = await _service.GetTrendStatisticsAsync(startYear, startMonth, monthCount);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(4);
|
||||
result[0].Year.Should().Be(2024);
|
||||
result[0].Month.Should().Be(11);
|
||||
result[1].Year.Should().Be(2024);
|
||||
result[1].Month.Should().Be(12);
|
||||
result[2].Year.Should().Be(2025);
|
||||
result[2].Month.Should().Be(1);
|
||||
result[3].Year.Should().Be(2025);
|
||||
result[3].Month.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetReasonGroupsAsync_基本测试()
|
||||
{
|
||||
// Arrange
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Reason = "麦当劳",
|
||||
Classify = "",
|
||||
Amount = -50m,
|
||||
Type = TransactionType.Expense,
|
||||
OccurredAt = new DateTime(2024, 1, 1)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Reason = "麦当劳",
|
||||
Classify = "",
|
||||
Amount = -80m,
|
||||
Type = TransactionType.Expense,
|
||||
OccurredAt = new DateTime(2024, 1, 2)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
Reason = "肯德基",
|
||||
Classify = "",
|
||||
Amount = -60m,
|
||||
Type = TransactionType.Expense,
|
||||
OccurredAt = new DateTime(2024, 1, 3)
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 4,
|
||||
Reason = "麦当劳",
|
||||
Classify = "快餐",
|
||||
Amount = -45m,
|
||||
Type = TransactionType.Expense,
|
||||
OccurredAt = new DateTime(2024, 1, 4)
|
||||
}
|
||||
};
|
||||
|
||||
_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(testData);
|
||||
|
||||
// Act
|
||||
var (list, total) = await _service.GetReasonGroupsAsync();
|
||||
|
||||
// Assert
|
||||
total.Should().Be(2);
|
||||
list.Should().HaveCount(2);
|
||||
|
||||
var mcdonalds = list.First(g => g.Reason == "麦当劳");
|
||||
mcdonalds.Count.Should().Be(2);
|
||||
mcdonalds.TotalAmount.Should().Be(130m);
|
||||
mcdonalds.SampleType.Should().Be(TransactionType.Expense);
|
||||
mcdonalds.SampleClassify.Should().Be("");
|
||||
mcdonalds.TransactionIds.Should().Contain(1L);
|
||||
mcdonalds.TransactionIds.Should().Contain(2L);
|
||||
|
||||
var kfc = list.First(g => g.Reason == "肯德基");
|
||||
kfc.Count.Should().Be(1);
|
||||
kfc.TotalAmount.Should().Be(60m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClassifiedByKeywordsWithScoreAsync_基本匹配()
|
||||
{
|
||||
// Arrange
|
||||
var keywords = new List<string> { "餐饮", "午餐" };
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Reason = "今天午餐吃得很饱",
|
||||
Classify = "餐饮",
|
||||
OccurredAt = new DateTime(2024, 1, 1),
|
||||
Amount = -50m
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Reason = "餐饮支出",
|
||||
Classify = "餐饮",
|
||||
OccurredAt = new DateTime(2024, 1, 2),
|
||||
Amount = -80m
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
Reason = "交通费",
|
||||
Classify = "交通",
|
||||
OccurredAt = new DateTime(2024, 1, 3),
|
||||
Amount = -10m
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.GetClassifiedByKeywordsAsync(Arg.Any<List<string>>(), Arg.Any<int>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetClassifiedByKeywordsWithScoreAsync(keywords, minMatchRate: 0.3, limit: 10);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
var first = result[0];
|
||||
// 第一个结果应该是相关性分数最高的,可能是 Id=2("今天午餐吃得很饱"匹配两个关键词)
|
||||
first.record.Id.Should().BeOneOf(1L, 2L);
|
||||
first.relevanceScore.Should().BeGreaterThan(0.5);
|
||||
|
||||
var second = result[1];
|
||||
second.record.Id.Should().BeOneOf(1L, 2L);
|
||||
second.record.Id.Should().NotBe(first.record.Id);
|
||||
second.relevanceScore.Should().BeGreaterThan(0.3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetClassifiedByKeywordsWithScoreAsync_精确匹配加分()
|
||||
{
|
||||
// Arrange
|
||||
var keywords = new List<string> { "午餐" };
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Reason = "午餐",
|
||||
Classify = "餐饮",
|
||||
OccurredAt = new DateTime(2024, 1, 1),
|
||||
Amount = -50m
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Reason = "今天中午吃了一顿午餐",
|
||||
Classify = "餐饮",
|
||||
OccurredAt = new DateTime(2024, 1, 2),
|
||||
Amount = -80m
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.GetClassifiedByKeywordsAsync(Arg.Any<List<string>>(), Arg.Any<int>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetClassifiedByKeywordsWithScoreAsync(keywords, minMatchRate: 0.3, limit: 10);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
// 精确匹配应该得分更高
|
||||
result[0].record.Id.Should().Be(1);
|
||||
result[0].relevanceScore.Should().BeGreaterThan(result[1].relevanceScore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFilteredTrendStatisticsAsync_按日分组()
|
||||
{
|
||||
// Arrange
|
||||
var startDate = new DateTime(2024, 1, 1);
|
||||
var endDate = new DateTime(2024, 1, 5);
|
||||
var type = TransactionType.Expense;
|
||||
var classifies = new[] { "餐饮", "交通" };
|
||||
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 10, 0, 0),
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 1, 1, 15, 0, 0),
|
||||
Amount = -50m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 1, 2, 10, 0, 0),
|
||||
Amount = -80m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
startDate,
|
||||
endDate,
|
||||
type,
|
||||
classifies,
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<bool>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetFilteredTrendStatisticsAsync(startDate, endDate, type, classifies, groupByMonth: false);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
result.Should().ContainKey(new DateTime(2024, 1, 1));
|
||||
result.Should().ContainKey(new DateTime(2024, 1, 2));
|
||||
|
||||
result[new DateTime(2024, 1, 1)].Should().Be(150m);
|
||||
result[new DateTime(2024, 1, 2)].Should().Be(80m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetFilteredTrendStatisticsAsync_按月分组()
|
||||
{
|
||||
// Arrange
|
||||
var startDate = new DateTime(2024, 1, 1);
|
||||
var endDate = new DateTime(2024, 3, 31);
|
||||
var type = TransactionType.Expense;
|
||||
var classifies = new[] { "餐饮" };
|
||||
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
OccurredAt = new DateTime(2024, 1, 15),
|
||||
Amount = -1000m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
OccurredAt = new DateTime(2024, 2, 15),
|
||||
Amount = -1500m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
OccurredAt = new DateTime(2024, 3, 15),
|
||||
Amount = -2000m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
startDate,
|
||||
endDate,
|
||||
type,
|
||||
classifies,
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<bool>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetFilteredTrendStatisticsAsync(startDate, endDate, type, classifies, groupByMonth: true);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
result.Should().ContainKey(new DateTime(2024, 1, 1));
|
||||
result.Should().ContainKey(new DateTime(2024, 2, 1));
|
||||
result.Should().ContainKey(new DateTime(2024, 3, 1));
|
||||
|
||||
result[new DateTime(2024, 1, 1)].Should().Be(1000m);
|
||||
result[new DateTime(2024, 2, 1)].Should().Be(1500m);
|
||||
result[new DateTime(2024, 3, 1)].Should().Be(2000m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAmountGroupByClassifyAsync_基本测试()
|
||||
{
|
||||
// Arrange
|
||||
var startTime = new DateTime(2024, 1, 1);
|
||||
var endTime = new DateTime(2024, 1, 31);
|
||||
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Amount = -50m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "餐饮"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 3,
|
||||
Amount = 5000m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "工资"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 4,
|
||||
Amount = -200m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "交通"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
startTime,
|
||||
endTime,
|
||||
Arg.Any<TransactionType>(),
|
||||
Arg.Any<string[]>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<bool>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAmountGroupByClassifyAsync(startTime, endTime);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(3);
|
||||
|
||||
result[("餐饮", TransactionType.Expense)].Should().Be(-150m);
|
||||
result[("工资", TransactionType.Income)].Should().Be(5000m);
|
||||
result[("交通", TransactionType.Expense)].Should().Be(-200m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAmountGroupByClassifyAsync_相同分类不同类型()
|
||||
{
|
||||
// Arrange
|
||||
var startTime = new DateTime(2024, 1, 1);
|
||||
var endTime = new DateTime(2024, 1, 31);
|
||||
|
||||
var testData = new List<TransactionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Amount = -100m,
|
||||
Type = TransactionType.Expense,
|
||||
Classify = "兼职"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 2,
|
||||
Amount = 500m,
|
||||
Type = TransactionType.Income,
|
||||
Classify = "兼职"
|
||||
}
|
||||
};
|
||||
|
||||
_transactionRepository.QueryAsync(
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
startTime,
|
||||
endTime,
|
||||
Arg.Any<TransactionType>(),
|
||||
Arg.Any<string[]>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<int>(),
|
||||
Arg.Any<bool>())
|
||||
.ReturnsForAnyArgs(testData);
|
||||
|
||||
// Act
|
||||
var result = await _service.GetAmountGroupByClassifyAsync(startTime, endTime);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(2);
|
||||
|
||||
result[("兼职", TransactionType.Expense)].Should().Be(-100m);
|
||||
result[("兼职", TransactionType.Income)].Should().Be(500m);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user