422 lines
16 KiB
C#
422 lines
16 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using WorkCameraExport.Forms;
|
||
using Xunit;
|
||
using SystemRandom = System.Random;
|
||
|
||
namespace WorkCameraExport.Tests
|
||
{
|
||
/// <summary>
|
||
/// 图片上传数量属性测试
|
||
/// Feature: work-camera-2.0.1, Property 3: 图片上传数量限制
|
||
/// Feature: work-camera-2.0.1, Property 4: 图片删除后列表更新
|
||
/// Validates: Requirements 4.5, 4.6
|
||
/// </summary>
|
||
public class ImageUploadPropertyTests
|
||
{
|
||
private const int MaxImageCount = 15;
|
||
|
||
#region Property 3: 图片上传数量限制
|
||
|
||
/// <summary>
|
||
/// Property 3: 图片上传数量限制
|
||
/// For any 上传操作,图片列表长度应始终不超过 15。
|
||
/// 当尝试添加图片使总数超过 15 时,应拒绝添加并提示用户。
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageUpload_ShouldNeverExceedMaxCount()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(0, 20).ToArbitrary(), // 初始图片数量 0-20
|
||
Gen.Choose(1, 10).ToArbitrary(), // 尝试添加的图片数量 1-10
|
||
(initialCount, addCount) =>
|
||
{
|
||
// 模拟图片列表
|
||
var imageList = new List<ImageItem>();
|
||
|
||
// 添加初始图片(模拟已有图片)
|
||
var actualInitial = Math.Min(initialCount, MaxImageCount);
|
||
for (int i = 0; i < actualInitial; i++)
|
||
{
|
||
imageList.Add(new ImageItem { SortOrder = i, IsExisting = true });
|
||
}
|
||
|
||
// 计算可添加的数量
|
||
var remainingSlots = MaxImageCount - imageList.Count;
|
||
var canAdd = Math.Min(addCount, remainingSlots);
|
||
|
||
// 模拟添加图片
|
||
for (int i = 0; i < canAdd; i++)
|
||
{
|
||
imageList.Add(new ImageItem { SortOrder = imageList.Count, IsExisting = false });
|
||
}
|
||
|
||
// 验证:图片数量不超过最大值
|
||
var countWithinLimit = imageList.Count <= MaxImageCount;
|
||
|
||
return countWithinLimit
|
||
.Label($"Initial: {actualInitial}, Tried to add: {addCount}, " +
|
||
$"Actually added: {canAdd}, Final count: {imageList.Count} (max: {MaxImageCount})");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3 扩展: 当已达到最大数量时,不应添加任何图片
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageUpload_WhenAtMax_ShouldNotAddMore()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(1, 10).ToArbitrary(), // 尝试添加的图片数量
|
||
(addCount) =>
|
||
{
|
||
// 模拟已满的图片列表
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < MaxImageCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem { SortOrder = i, IsExisting = true });
|
||
}
|
||
|
||
var initialCount = imageList.Count;
|
||
|
||
// 计算可添加的数量(应该是0)
|
||
var remainingSlots = MaxImageCount - imageList.Count;
|
||
var canAdd = Math.Min(addCount, remainingSlots);
|
||
|
||
// 验证:不应添加任何图片
|
||
var noAddition = canAdd == 0;
|
||
var countUnchanged = imageList.Count == initialCount;
|
||
|
||
return (noAddition && countUnchanged)
|
||
.Label($"Tried to add: {addCount}, Can add: {canAdd}, " +
|
||
$"Count unchanged: {countUnchanged}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3 扩展: 添加的图片数量应等于 min(请求数量, 剩余槽位)
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageUpload_AddedCount_ShouldEqualMinOfRequestedAndRemaining()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(0, MaxImageCount).ToArbitrary(), // 初始图片数量
|
||
Gen.Choose(1, 20).ToArbitrary(), // 尝试添加的图片数量
|
||
(initialCount, addCount) =>
|
||
{
|
||
var remainingSlots = MaxImageCount - initialCount;
|
||
var expectedAdded = Math.Min(addCount, Math.Max(0, remainingSlots));
|
||
var expectedFinal = initialCount + expectedAdded;
|
||
|
||
// 验证计算逻辑
|
||
var addedCorrect = expectedAdded == Math.Min(addCount, Math.Max(0, remainingSlots));
|
||
var finalCorrect = expectedFinal <= MaxImageCount;
|
||
|
||
return (addedCorrect && finalCorrect)
|
||
.Label($"Initial: {initialCount}, Requested: {addCount}, " +
|
||
$"Expected added: {expectedAdded}, Expected final: {expectedFinal}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3 扩展: 边界值测试 - 恰好 15 张图片
|
||
/// </summary>
|
||
[Fact]
|
||
public void ImageUpload_ExactlyMax_ShouldNotAllowMore()
|
||
{
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < MaxImageCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem { SortOrder = i });
|
||
}
|
||
|
||
var remainingSlots = MaxImageCount - imageList.Count;
|
||
|
||
Assert.Equal(MaxImageCount, imageList.Count);
|
||
Assert.Equal(0, remainingSlots);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 3 扩展: 边界值测试 - 14 张图片,添加 1 张
|
||
/// </summary>
|
||
[Fact]
|
||
public void ImageUpload_OneBelowMax_ShouldAllowOneMore()
|
||
{
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < MaxImageCount - 1; i++)
|
||
{
|
||
imageList.Add(new ImageItem { SortOrder = i });
|
||
}
|
||
|
||
var remainingSlots = MaxImageCount - imageList.Count;
|
||
|
||
Assert.Equal(MaxImageCount - 1, imageList.Count);
|
||
Assert.Equal(1, remainingSlots);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Property 4: 图片删除后列表更新
|
||
|
||
/// <summary>
|
||
/// Property 4: 图片删除后列表更新
|
||
/// For any 图片删除操作,删除后图片列表长度应减少 1,
|
||
/// 且被删除的图片不应存在于列表中。
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageDelete_ShouldReduceCountByOne()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(1, MaxImageCount).ToArbitrary(), // 初始图片数量 1-15
|
||
(initialCount) =>
|
||
{
|
||
// 创建图片列表
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < initialCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem
|
||
{
|
||
Id = i,
|
||
SortOrder = i,
|
||
Url = $"http://example.com/image{i}.jpg"
|
||
});
|
||
}
|
||
|
||
// 随机选择一个索引删除
|
||
var deleteIndex = new SystemRandom().Next(0, imageList.Count);
|
||
var deletedItem = imageList[deleteIndex];
|
||
|
||
// 执行删除
|
||
imageList.RemoveAt(deleteIndex);
|
||
|
||
// 验证:数量减少 1
|
||
var countReduced = imageList.Count == initialCount - 1;
|
||
|
||
// 验证:被删除的图片不在列表中
|
||
var itemRemoved = !imageList.Any(i => i.Id == deletedItem.Id);
|
||
|
||
return (countReduced && itemRemoved)
|
||
.Label($"Initial: {initialCount}, After delete: {imageList.Count}, " +
|
||
$"Deleted item removed: {itemRemoved}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 删除后排序应保持连续
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageDelete_ShouldMaintainSortOrder()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(2, MaxImageCount).ToArbitrary(), // 初始图片数量 2-15(至少2张才能测试删除后排序)
|
||
(initialCount) =>
|
||
{
|
||
// 创建图片列表
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < initialCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem { Id = i, SortOrder = i });
|
||
}
|
||
|
||
// 随机选择一个索引删除
|
||
var deleteIndex = new SystemRandom().Next(0, imageList.Count);
|
||
imageList.RemoveAt(deleteIndex);
|
||
|
||
// 重新排序
|
||
for (int i = 0; i < imageList.Count; i++)
|
||
{
|
||
imageList[i].SortOrder = i;
|
||
}
|
||
|
||
// 验证:排序是连续的 0, 1, 2, ...
|
||
var sortOrderCorrect = true;
|
||
for (int i = 0; i < imageList.Count; i++)
|
||
{
|
||
if (imageList[i].SortOrder != i)
|
||
{
|
||
sortOrderCorrect = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return sortOrderCorrect
|
||
.Label($"Initial: {initialCount}, After delete: {imageList.Count}, " +
|
||
$"Sort order correct: {sortOrderCorrect}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 删除所有图片后列表应为空
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageDelete_AllImages_ShouldResultInEmptyList()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(1, MaxImageCount).ToArbitrary(), // 初始图片数量
|
||
(initialCount) =>
|
||
{
|
||
// 创建图片列表
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < initialCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem { Id = i, SortOrder = i });
|
||
}
|
||
|
||
// 删除所有图片
|
||
while (imageList.Count > 0)
|
||
{
|
||
imageList.RemoveAt(0);
|
||
}
|
||
|
||
// 验证:列表为空
|
||
return (imageList.Count == 0)
|
||
.Label($"Initial: {initialCount}, After deleting all: {imageList.Count}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 删除无效索引应不改变列表
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageDelete_InvalidIndex_ShouldNotChangeList()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(1, MaxImageCount).ToArbitrary(), // 初始图片数量
|
||
Gen.Choose(-10, -1).ToArbitrary(), // 无效的负索引
|
||
(initialCount, invalidIndex) =>
|
||
{
|
||
// 创建图片列表
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < initialCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem { Id = i, SortOrder = i });
|
||
}
|
||
|
||
// 尝试删除无效索引(模拟边界检查)
|
||
var canDelete = invalidIndex >= 0 && invalidIndex < imageList.Count;
|
||
|
||
// 验证:无效索引不应允许删除
|
||
return (!canDelete)
|
||
.Label($"Initial: {initialCount}, Invalid index: {invalidIndex}, " +
|
||
$"Can delete: {canDelete}");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 删除后可以继续添加图片
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ImageDelete_ThenAdd_ShouldWork()
|
||
{
|
||
return Prop.ForAll(
|
||
Gen.Choose(1, MaxImageCount).ToArbitrary(), // 初始图片数量
|
||
Gen.Choose(1, 5).ToArbitrary(), // 删除数量
|
||
Gen.Choose(1, 5).ToArbitrary(), // 添加数量
|
||
(initialCount, deleteCount, addCount) =>
|
||
{
|
||
// 创建图片列表
|
||
var imageList = new List<ImageItem>();
|
||
for (int i = 0; i < initialCount; i++)
|
||
{
|
||
imageList.Add(new ImageItem { Id = i, SortOrder = i });
|
||
}
|
||
|
||
// 删除图片
|
||
var actualDelete = Math.Min(deleteCount, imageList.Count);
|
||
for (int i = 0; i < actualDelete; i++)
|
||
{
|
||
if (imageList.Count > 0)
|
||
{
|
||
imageList.RemoveAt(0);
|
||
}
|
||
}
|
||
|
||
var afterDelete = imageList.Count;
|
||
|
||
// 添加图片
|
||
var remainingSlots = MaxImageCount - imageList.Count;
|
||
var actualAdd = Math.Min(addCount, remainingSlots);
|
||
for (int i = 0; i < actualAdd; i++)
|
||
{
|
||
imageList.Add(new ImageItem { Id = 100 + i, SortOrder = imageList.Count });
|
||
}
|
||
|
||
var finalCount = imageList.Count;
|
||
|
||
// 验证:最终数量正确
|
||
var expectedFinal = afterDelete + actualAdd;
|
||
var countCorrect = finalCount == expectedFinal;
|
||
var withinLimit = finalCount <= MaxImageCount;
|
||
|
||
return (countCorrect && withinLimit)
|
||
.Label($"Initial: {initialCount}, Deleted: {actualDelete}, " +
|
||
$"Added: {actualAdd}, Final: {finalCount} (expected: {expectedFinal})");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 边界值测试 - 删除唯一的图片
|
||
/// </summary>
|
||
[Fact]
|
||
public void ImageDelete_SingleImage_ShouldResultInEmptyList()
|
||
{
|
||
var imageList = new List<ImageItem>
|
||
{
|
||
new ImageItem { Id = 1, SortOrder = 0 }
|
||
};
|
||
|
||
imageList.RemoveAt(0);
|
||
|
||
Assert.Empty(imageList);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 边界值测试 - 删除第一张图片
|
||
/// </summary>
|
||
[Fact]
|
||
public void ImageDelete_FirstImage_ShouldShiftOthers()
|
||
{
|
||
var imageList = new List<ImageItem>
|
||
{
|
||
new ImageItem { Id = 1, SortOrder = 0 },
|
||
new ImageItem { Id = 2, SortOrder = 1 },
|
||
new ImageItem { Id = 3, SortOrder = 2 }
|
||
};
|
||
|
||
imageList.RemoveAt(0);
|
||
|
||
// 重新排序
|
||
for (int i = 0; i < imageList.Count; i++)
|
||
{
|
||
imageList[i].SortOrder = i;
|
||
}
|
||
|
||
Assert.Equal(2, imageList.Count);
|
||
Assert.Equal(2, imageList[0].Id);
|
||
Assert.Equal(0, imageList[0].SortOrder);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Property 4 扩展: 边界值测试 - 删除最后一张图片
|
||
/// </summary>
|
||
[Fact]
|
||
public void ImageDelete_LastImage_ShouldNotAffectOthers()
|
||
{
|
||
var imageList = new List<ImageItem>
|
||
{
|
||
new ImageItem { Id = 1, SortOrder = 0 },
|
||
new ImageItem { Id = 2, SortOrder = 1 },
|
||
new ImageItem { Id = 3, SortOrder = 2 }
|
||
};
|
||
|
||
imageList.RemoveAt(2);
|
||
|
||
Assert.Equal(2, imageList.Count);
|
||
Assert.Equal(1, imageList[0].Id);
|
||
Assert.Equal(2, imageList[1].Id);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|