vending-machine/backend/tests/VendingMachine.Tests/PointsPropertyTests.cs
2026-04-03 06:07:13 +08:00

173 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}