173 lines
6.7 KiB
C#
173 lines
6.7 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||
using Microsoft.Extensions.Logging;
|
||
using Moq;
|
||
using VendingMachine.Application.DTOs.Points;
|
||
using VendingMachine.Application.Services;
|
||
using VendingMachine.Domain.Entities;
|
||
using VendingMachine.Infrastructure.Data;
|
||
using VendingMachine.Infrastructure.Services;
|
||
|
||
namespace VendingMachine.Tests;
|
||
|
||
/// <summary>
|
||
/// 积分模块属性测试
|
||
/// </summary>
|
||
public class PointsPropertyTests
|
||
{
|
||
// Feature: vending-machine-app, Property 10: 积分转换计算
|
||
// 对于任意支付金额和转换比,计算出的积分应等于 Math.Floor(金额 * 转换比)。
|
||
// **Validates: Requirements 5.1**
|
||
[Property(MaxTest = 100)]
|
||
public bool PointsConversion_ShouldMatchFloorFormula(PositiveInt amountRaw, PositiveInt rateRaw)
|
||
{
|
||
// 生成合理范围的金额(0.01 ~ 10000.00)和转换比(0.1 ~ 10.0)
|
||
var amount = (decimal)(amountRaw.Get % 1000000 + 1) / 100m;
|
||
var rate = (decimal)(rateRaw.Get % 100 + 1) / 10m;
|
||
|
||
var points = IPointsService.CalculatePoints(amount, rate);
|
||
var expected = (int)Math.Floor(amount * rate);
|
||
|
||
return points == expected;
|
||
}
|
||
|
||
[Property(MaxTest = 100)]
|
||
public bool PointsConversion_ShouldAlwaysBeNonNegative(PositiveInt amountRaw, PositiveInt rateRaw)
|
||
{
|
||
var amount = (decimal)(amountRaw.Get % 1000000 + 1) / 100m;
|
||
var rate = (decimal)(rateRaw.Get % 100 + 1) / 10m;
|
||
|
||
var points = IPointsService.CalculatePoints(amount, rate);
|
||
return points >= 0;
|
||
}
|
||
|
||
// Feature: vending-machine-app, Property 11: 积分记录渲染完整性
|
||
// 对于任意积分记录,获取记录应包含来源、时间和增加数量;使用记录应包含使用方式、时间和减少数量。
|
||
// **Validates: Requirements 5.4, 5.5**
|
||
[Property(MaxTest = 100)]
|
||
public bool EarnRecord_ShouldContainAllRequiredFields(
|
||
NonEmptyString source, PositiveInt amount)
|
||
{
|
||
var record = new PointRecordDto
|
||
{
|
||
Id = Guid.NewGuid().ToString(),
|
||
Type = "earn",
|
||
Amount = amount.Get,
|
||
Source = source.Get,
|
||
CreatedAt = DateTime.UtcNow
|
||
};
|
||
|
||
// 获取记录应包含来源、时间和增加数量
|
||
return !string.IsNullOrEmpty(record.Source)
|
||
&& record.CreatedAt != default
|
||
&& record.Amount > 0
|
||
&& record.Type == "earn";
|
||
}
|
||
|
||
[Property(MaxTest = 100)]
|
||
public bool SpendRecord_ShouldContainAllRequiredFields(
|
||
NonEmptyString source, PositiveInt amount)
|
||
{
|
||
var record = new PointRecordDto
|
||
{
|
||
Id = Guid.NewGuid().ToString(),
|
||
Type = "spend",
|
||
Amount = amount.Get,
|
||
Source = source.Get,
|
||
CreatedAt = DateTime.UtcNow
|
||
};
|
||
|
||
// 使用记录应包含使用方式、时间和减少数量
|
||
return !string.IsNullOrEmpty(record.Source)
|
||
&& record.CreatedAt != default
|
||
&& record.Amount > 0
|
||
&& record.Type == "spend";
|
||
}
|
||
|
||
// Feature: vending-machine-app, Property 12: 赠送积分守恒
|
||
// 对于任意积分赠送操作,如果发送方积分充足,则发送方减少的积分等于接收方增加的积分(总积分守恒);
|
||
// 如果发送方积分不足,则双方积分均不变。
|
||
// **Validates: Requirements 5.6, 5.7**
|
||
[Property(MaxTest = 100)]
|
||
public bool GiftPoints_WhenSufficientBalance_TotalPointsConserved(PositiveInt balanceRaw, PositiveInt giftRaw)
|
||
{
|
||
// 确保发送方积分充足
|
||
var balance = balanceRaw.Get % 10000 + 1;
|
||
var giftAmount = (giftRaw.Get % balance) + 1;
|
||
|
||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||
.UseInMemoryDatabase(databaseName: $"GiftSufficient_{Guid.NewGuid()}")
|
||
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
||
.Options;
|
||
|
||
using var db = new AppDbContext(options);
|
||
var logger = Mock.Of<ILogger<PointsService>>();
|
||
|
||
var sender = new User { Uid = "sender000001", Phone = "13800000001", AreaCode = "+86", Nickname = "发送方", PointsBalance = balance };
|
||
var receiver = new User { Uid = "receiver0001", Phone = "13800000002", AreaCode = "+86", Nickname = "接收方", PointsBalance = 0 };
|
||
db.Users.AddRange(sender, receiver);
|
||
db.SaveChanges();
|
||
|
||
var totalBefore = sender.PointsBalance + receiver.PointsBalance;
|
||
|
||
var service = new PointsService(db, logger);
|
||
var result = service.GiftPointsAsync("sender000001", new GiftPointsRequest
|
||
{
|
||
TargetUid = "receiver0001",
|
||
Amount = giftAmount
|
||
}).GetAwaiter().GetResult();
|
||
|
||
db.Entry(sender).Reload();
|
||
db.Entry(receiver).Reload();
|
||
|
||
var totalAfter = sender.PointsBalance + receiver.PointsBalance;
|
||
|
||
// 总积分守恒
|
||
return result.Success
|
||
&& totalBefore == totalAfter
|
||
&& sender.PointsBalance == balance - giftAmount
|
||
&& receiver.PointsBalance == giftAmount;
|
||
}
|
||
|
||
[Property(MaxTest = 100)]
|
||
public bool GiftPoints_WhenInsufficientBalance_BothUnchanged(PositiveInt balanceRaw, PositiveInt extraRaw)
|
||
{
|
||
// 确保发送方积分不足
|
||
var balance = balanceRaw.Get % 100 + 1;
|
||
var giftAmount = balance + (extraRaw.Get % 1000) + 1;
|
||
|
||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||
.UseInMemoryDatabase(databaseName: $"GiftInsufficient_{Guid.NewGuid()}")
|
||
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
|
||
.Options;
|
||
|
||
using var db = new AppDbContext(options);
|
||
var logger = Mock.Of<ILogger<PointsService>>();
|
||
|
||
var sender = new User { Uid = "sender000001", Phone = "13800000001", AreaCode = "+86", Nickname = "发送方", PointsBalance = balance };
|
||
var receiver = new User { Uid = "receiver0001", Phone = "13800000002", AreaCode = "+86", Nickname = "接收方", PointsBalance = 50 };
|
||
db.Users.AddRange(sender, receiver);
|
||
db.SaveChanges();
|
||
|
||
var senderBefore = sender.PointsBalance;
|
||
var receiverBefore = receiver.PointsBalance;
|
||
|
||
var service = new PointsService(db, logger);
|
||
var result = service.GiftPointsAsync("sender000001", new GiftPointsRequest
|
||
{
|
||
TargetUid = "receiver0001",
|
||
Amount = giftAmount
|
||
}).GetAwaiter().GetResult();
|
||
|
||
db.Entry(sender).Reload();
|
||
db.Entry(receiver).Reload();
|
||
|
||
// 积分不足时双方积分均不变
|
||
return !result.Success
|
||
&& sender.PointsBalance == senderBefore
|
||
&& receiver.PointsBalance == receiverBefore;
|
||
}
|
||
}
|