540 lines
18 KiB
C#
540 lines
18 KiB
C#
using FsCheck;
|
||
using FsCheck.Xunit;
|
||
using Xunit;
|
||
using XiangYi.Application.DTOs.Requests;
|
||
using XiangYi.Application.Services;
|
||
using XiangYi.Core.Entities.Biz;
|
||
using XiangYi.Core.Enums;
|
||
|
||
namespace XiangYi.Application.Tests.Services;
|
||
|
||
/// <summary>
|
||
/// SearchService属性测试 - 搜索条件过滤正确性
|
||
/// </summary>
|
||
public class SearchCriteriaFilterPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 12: 搜索条件过滤正确性**
|
||
/// **Validates: Requirements 5.1**
|
||
///
|
||
/// *For any* 搜索请求, 返回的用户列表中每个用户都应满足所有搜索条件
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property SearchCriteria_AllMatchedUsersShouldSatisfyAllConditions()
|
||
{
|
||
// 生成有效的用户资料参数
|
||
var birthYearArb = Gen.Choose(1970, 2005);
|
||
var heightArb = Gen.Choose(150, 200);
|
||
|
||
return Prop.ForAll(
|
||
birthYearArb.ToArbitrary(),
|
||
heightArb.ToArbitrary(),
|
||
(birthYear, height) =>
|
||
{
|
||
// 创建用户资料
|
||
var profile = CreateBasicProfile(birthYear, height);
|
||
|
||
// 创建空搜索条件
|
||
var request = new SearchRequest();
|
||
|
||
// Act
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// Assert - 空条件应匹配所有用户
|
||
return matches;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 年龄过滤应正确工作
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property AgeFilter_ShouldWorkCorrectly()
|
||
{
|
||
var birthYearArb = Gen.Choose(1970, 2005);
|
||
var ageMinArb = Gen.Choose(18, 40);
|
||
var ageMaxArb = Gen.Choose(25, 60);
|
||
|
||
return Prop.ForAll(
|
||
birthYearArb.ToArbitrary(),
|
||
ageMinArb.ToArbitrary(),
|
||
ageMaxArb.ToArbitrary(),
|
||
(birthYear, ageMin, ageMax) =>
|
||
{
|
||
// 确保ageMin <= ageMax
|
||
var actualMin = Math.Min(ageMin, ageMax);
|
||
var actualMax = Math.Max(ageMin, ageMax);
|
||
|
||
var profile = CreateBasicProfile(birthYear);
|
||
var request = new SearchRequest { AgeMin = actualMin, AgeMax = actualMax };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
var age = DateTime.Now.Year - birthYear;
|
||
|
||
// 验证:匹配当且仅当年龄在范围内
|
||
var expectedMatch = age >= actualMin && age <= actualMax;
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 身高过滤应正确工作
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property HeightFilter_ShouldWorkCorrectly()
|
||
{
|
||
var heightArb = Gen.Choose(150, 200);
|
||
var heightMinArb = Gen.Choose(155, 175);
|
||
var heightMaxArb = Gen.Choose(170, 195);
|
||
|
||
return Prop.ForAll(
|
||
heightArb.ToArbitrary(),
|
||
heightMinArb.ToArbitrary(),
|
||
heightMaxArb.ToArbitrary(),
|
||
(height, heightMin, heightMax) =>
|
||
{
|
||
// 确保heightMin <= heightMax
|
||
var actualMin = Math.Min(heightMin, heightMax);
|
||
var actualMax = Math.Max(heightMin, heightMax);
|
||
|
||
var profile = CreateBasicProfile(1990);
|
||
profile.Height = height;
|
||
|
||
var request = new SearchRequest { HeightMin = actualMin, HeightMax = actualMax };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 验证:匹配当且仅当身高在范围内
|
||
var expectedMatch = height >= actualMin && height <= actualMax;
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 月收入过滤应正确工作
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property MonthlyIncomeFilter_ShouldWorkCorrectly()
|
||
{
|
||
var incomeArb = Gen.Choose(1, 10);
|
||
var incomeMinArb = Gen.Choose(1, 5);
|
||
var incomeMaxArb = Gen.Choose(5, 10);
|
||
|
||
return Prop.ForAll(
|
||
incomeArb.ToArbitrary(),
|
||
incomeMinArb.ToArbitrary(),
|
||
incomeMaxArb.ToArbitrary(),
|
||
(income, incomeMin, incomeMax) =>
|
||
{
|
||
// 确保incomeMin <= incomeMax
|
||
var actualMin = Math.Min(incomeMin, incomeMax);
|
||
var actualMax = Math.Max(incomeMin, incomeMax);
|
||
|
||
var profile = CreateBasicProfile(1990);
|
||
profile.MonthlyIncome = income;
|
||
|
||
var request = new SearchRequest { MonthlyIncomeMin = actualMin, MonthlyIncomeMax = actualMax };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 验证:匹配当且仅当收入在范围内
|
||
var expectedMatch = income >= actualMin && income <= actualMax;
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 工作城市过滤应正确工作
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property WorkCityFilter_ShouldWorkCorrectly()
|
||
{
|
||
var cities = new[] { "北京", "上海", "广州", "深圳", "杭州" };
|
||
var cityArb = Gen.Elements(cities);
|
||
|
||
return Prop.ForAll(
|
||
cityArb.ToArbitrary(),
|
||
cityArb.ToArbitrary(),
|
||
(profileCity, searchCity) =>
|
||
{
|
||
var profile = CreateBasicProfile(1990);
|
||
profile.WorkCity = profileCity;
|
||
|
||
var request = new SearchRequest { WorkCity = searchCity };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 验证:匹配当且仅当城市相同(忽略大小写)
|
||
var expectedMatch = string.Equals(profileCity, searchCity, StringComparison.OrdinalIgnoreCase);
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 房产情况过滤应正确工作(OR逻辑)
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property HouseStatusFilter_ShouldWorkWithOrLogic()
|
||
{
|
||
var houseStatusArb = Gen.Choose(1, 6);
|
||
var houseStatus1Arb = Gen.Choose(1, 3);
|
||
var houseStatus2Arb = Gen.Choose(4, 6);
|
||
|
||
return Prop.ForAll(
|
||
houseStatusArb.ToArbitrary(),
|
||
houseStatus1Arb.ToArbitrary(),
|
||
houseStatus2Arb.ToArbitrary(),
|
||
(profileHouseStatus, status1, status2) =>
|
||
{
|
||
var searchHouseStatuses = new List<int> { status1, status2 };
|
||
|
||
var profile = CreateBasicProfile(1990);
|
||
profile.HouseStatus = profileHouseStatus;
|
||
|
||
var request = new SearchRequest { HouseStatus = searchHouseStatuses };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 验证:匹配当且仅当资料的房产状态在搜索列表中(OR逻辑)
|
||
var expectedMatch = searchHouseStatuses.Contains(profileHouseStatus);
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 车辆情况过滤应正确工作(OR逻辑)
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property CarStatusFilter_ShouldWorkWithOrLogic()
|
||
{
|
||
var carStatusArb = Gen.Choose(1, 3);
|
||
var carStatus1Arb = Gen.Choose(1, 2);
|
||
var carStatus2Arb = Gen.Choose(2, 3);
|
||
|
||
return Prop.ForAll(
|
||
carStatusArb.ToArbitrary(),
|
||
carStatus1Arb.ToArbitrary(),
|
||
carStatus2Arb.ToArbitrary(),
|
||
(profileCarStatus, status1, status2) =>
|
||
{
|
||
var searchCarStatuses = new List<int> { status1, status2 }.Distinct().ToList();
|
||
|
||
var profile = CreateBasicProfile(1990);
|
||
profile.CarStatus = profileCarStatus;
|
||
|
||
var request = new SearchRequest { CarStatus = searchCarStatuses };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 验证:匹配当且仅当资料的车辆状态在搜索列表中(OR逻辑)
|
||
var expectedMatch = searchCarStatuses.Contains(profileCarStatus);
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 婚姻状态过滤应正确工作(OR逻辑)
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property MarriageStatusFilter_ShouldWorkWithOrLogic()
|
||
{
|
||
var marriageStatusArb = Gen.Choose(1, 3);
|
||
var marriageStatus1Arb = Gen.Choose(1, 2);
|
||
var marriageStatus2Arb = Gen.Choose(2, 3);
|
||
|
||
return Prop.ForAll(
|
||
marriageStatusArb.ToArbitrary(),
|
||
marriageStatus1Arb.ToArbitrary(),
|
||
marriageStatus2Arb.ToArbitrary(),
|
||
(profileMarriageStatus, status1, status2) =>
|
||
{
|
||
var searchMarriageStatuses = new List<int> { status1, status2 }.Distinct().ToList();
|
||
|
||
var profile = CreateBasicProfile(1990);
|
||
profile.MarriageStatus = profileMarriageStatus;
|
||
|
||
var request = new SearchRequest { MarriageStatus = searchMarriageStatuses };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 验证:匹配当且仅当资料的婚姻状态在搜索列表中(OR逻辑)
|
||
var expectedMatch = searchMarriageStatuses.Contains(profileMarriageStatus);
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 空搜索条件应匹配所有用户
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property EmptySearchCriteria_ShouldMatchAllUsers()
|
||
{
|
||
var birthYearArb = Gen.Choose(1970, 2005);
|
||
|
||
return Prop.ForAll(
|
||
birthYearArb.ToArbitrary(),
|
||
birthYear =>
|
||
{
|
||
var profile = CreateBasicProfile(birthYear);
|
||
var request = new SearchRequest(); // 空搜索条件
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 空条件应匹配所有用户
|
||
return matches;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 诚意会员每日搜索次数限制应为5
|
||
/// </summary>
|
||
[Fact]
|
||
public void SincereMemberDailySearchLimit_ShouldBe5()
|
||
{
|
||
Assert.Equal(5, SearchService.SincereMemberDailySearchLimit);
|
||
}
|
||
|
||
#region Helper Methods
|
||
|
||
private static UserProfile CreateBasicProfile(int birthYear, int height = 175, int education = 4, int income = 5)
|
||
{
|
||
return new UserProfile
|
||
{
|
||
Id = 1,
|
||
UserId = 1,
|
||
BirthYear = birthYear,
|
||
Height = height,
|
||
Education = education,
|
||
MonthlyIncome = income,
|
||
HouseStatus = 1,
|
||
CarStatus = 1,
|
||
MarriageStatus = 1,
|
||
WorkCity = "北京",
|
||
WorkProvince = "北京",
|
||
ChildGender = 1,
|
||
Relationship = 1,
|
||
Surname = "张",
|
||
Occupation = "工程师",
|
||
Weight = 70,
|
||
ExpectMarryTime = 1,
|
||
Introduction = "测试",
|
||
IsPhotoPublic = true,
|
||
WeChatNo = "test123",
|
||
AuditStatus = (int)AuditStatus.Approved
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// SearchService属性测试 - 多选学历OR逻辑
|
||
/// </summary>
|
||
public class EducationOrLogicPropertyTests
|
||
{
|
||
/// <summary>
|
||
/// **Feature: backend-api, Property 13: 多选学历OR逻辑**
|
||
/// **Validates: Requirements 5.4**
|
||
///
|
||
/// *For any* 包含多个学历条件的搜索, 返回的用户应满足其中任一学历条件
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property EducationFilter_ShouldUseOrLogic()
|
||
{
|
||
// 生成用户的学历(1-6)
|
||
var profileEducationArb = Gen.Choose(1, 6);
|
||
// 生成两个不同的学历用于搜索
|
||
var edu1Arb = Gen.Choose(1, 3);
|
||
var edu2Arb = Gen.Choose(4, 6);
|
||
|
||
return Prop.ForAll(
|
||
profileEducationArb.ToArbitrary(),
|
||
edu1Arb.ToArbitrary(),
|
||
edu2Arb.ToArbitrary(),
|
||
(profileEducation, edu1, edu2) =>
|
||
{
|
||
// Arrange
|
||
var searchEducations = new List<int> { edu1, edu2 };
|
||
var profile = CreateBasicProfile(profileEducation);
|
||
var request = new SearchRequest { Education = searchEducations };
|
||
|
||
// Act
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// Assert - 匹配当且仅当用户学历在搜索列表中(OR逻辑)
|
||
var expectedMatch = searchEducations.Contains(profileEducation);
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单个学历条件应正确匹配
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property SingleEducation_ShouldMatchExactly()
|
||
{
|
||
var educationArb = Gen.Choose(1, 6);
|
||
|
||
return Prop.ForAll(
|
||
educationArb.ToArbitrary(),
|
||
educationArb.ToArbitrary(),
|
||
(profileEducation, searchEducation) =>
|
||
{
|
||
var profile = CreateBasicProfile(profileEducation);
|
||
var request = new SearchRequest { Education = new List<int> { searchEducation } };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 单个学历条件:匹配当且仅当学历相同
|
||
var expectedMatch = profileEducation == searchEducation;
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 多个学历条件应使用OR逻辑 - 三个学历
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property ThreeEducations_ShouldMatchAny()
|
||
{
|
||
var profileEducationArb = Gen.Choose(1, 6);
|
||
var edu1Arb = Gen.Choose(1, 2);
|
||
var edu2Arb = Gen.Choose(3, 4);
|
||
var edu3Arb = Gen.Choose(5, 6);
|
||
|
||
return Prop.ForAll(
|
||
profileEducationArb.ToArbitrary(),
|
||
edu1Arb.ToArbitrary(),
|
||
edu2Arb.ToArbitrary(),
|
||
(profileEducation, edu1, edu2) =>
|
||
{
|
||
var searchEducations = new List<int> { edu1, edu2, 5 }; // 固定第三个为5
|
||
var profile = CreateBasicProfile(profileEducation);
|
||
var request = new SearchRequest { Education = searchEducations };
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 多个学历条件:匹配当且仅当用户学历在列表中任一
|
||
var expectedMatch = searchEducations.Contains(profileEducation);
|
||
return matches == expectedMatch;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 空学历列表应匹配所有用户
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property EmptyEducationList_ShouldMatchAll()
|
||
{
|
||
var profileEducationArb = Gen.Choose(1, 6);
|
||
|
||
return Prop.ForAll(
|
||
profileEducationArb.ToArbitrary(),
|
||
profileEducation =>
|
||
{
|
||
var profile = CreateBasicProfile(profileEducation);
|
||
var request = new SearchRequest { Education = new List<int>() }; // 空列表
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 空学历列表应匹配所有用户
|
||
return matches;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// null学历条件应匹配所有用户
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property NullEducation_ShouldMatchAll()
|
||
{
|
||
var profileEducationArb = Gen.Choose(1, 6);
|
||
|
||
return Prop.ForAll(
|
||
profileEducationArb.ToArbitrary(),
|
||
profileEducation =>
|
||
{
|
||
var profile = CreateBasicProfile(profileEducation);
|
||
var request = new SearchRequest { Education = null }; // null
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// null学历条件应匹配所有用户
|
||
return matches;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 包含所有学历的列表应匹配所有用户
|
||
/// </summary>
|
||
[Property(MaxTest = 100)]
|
||
public Property AllEducations_ShouldMatchAll()
|
||
{
|
||
var profileEducationArb = Gen.Choose(1, 6);
|
||
|
||
return Prop.ForAll(
|
||
profileEducationArb.ToArbitrary(),
|
||
profileEducation =>
|
||
{
|
||
var profile = CreateBasicProfile(profileEducation);
|
||
var request = new SearchRequest
|
||
{
|
||
Education = new List<int> { 1, 2, 3, 4, 5, 6 } // 所有学历
|
||
};
|
||
|
||
var matches = SearchService.MatchesSearchCriteriaStatic(profile, request);
|
||
|
||
// 包含所有学历的列表应匹配所有用户
|
||
return matches;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 学历枚举值验证
|
||
/// </summary>
|
||
[Fact]
|
||
public void EducationEnum_ShouldHaveCorrectValues()
|
||
{
|
||
Assert.Equal(0, (int)Education.None);
|
||
Assert.Equal(1, (int)Education.HighSchool);
|
||
Assert.Equal(2, (int)Education.Technical);
|
||
Assert.Equal(3, (int)Education.College);
|
||
Assert.Equal(4, (int)Education.Bachelor);
|
||
Assert.Equal(5, (int)Education.Master);
|
||
Assert.Equal(6, (int)Education.Doctor);
|
||
}
|
||
|
||
#region Helper Methods
|
||
|
||
private static UserProfile CreateBasicProfile(int education)
|
||
{
|
||
return new UserProfile
|
||
{
|
||
Id = 1,
|
||
UserId = 1,
|
||
BirthYear = 1990,
|
||
Height = 175,
|
||
Education = education,
|
||
MonthlyIncome = 5,
|
||
HouseStatus = 1,
|
||
CarStatus = 1,
|
||
MarriageStatus = 1,
|
||
WorkCity = "北京",
|
||
WorkProvince = "北京",
|
||
ChildGender = 1,
|
||
Relationship = 1,
|
||
Surname = "张",
|
||
Occupation = "工程师",
|
||
Weight = 70,
|
||
ExpectMarryTime = 1,
|
||
Introduction = "测试",
|
||
IsPhotoPublic = true,
|
||
WeChatNo = "test123",
|
||
AuditStatus = (int)AuditStatus.Approved
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
}
|