使用 FsCheck 进行测试的方法
Approach to testing with FsCheck
我正在尝试将范式转变为 FsCheck 和基于 属性 的随机测试。我有复杂的业务工作流程,其中的测试用例比我可能列举的要多,而且业务逻辑是一个不断添加新功能的移动目标。
背景:配对是企业资源规划 (ERP) 系统中非常常见的抽象概念。订单履行、供应链物流等
示例:给定一个 C 和一个 P,确定两者是否匹配。在任何给定的时间点,一些 Ps 是 never 可匹配的,而一些 C 是 never 可匹配的。每个人都有一个状态,表明他们是否可以被考虑参加比赛。
public enum ObjectType {
C = 0,
P = 1
}
public enum CheckType {
CertA = 0,
CertB = 1
}
public class Check {
public CheckType CheckType {get; set;}
public ObjectType ObjectType {get; set;}
/* If ObjectType == CrossReferenceObjectType, then it is assumed to be self-referential and there is no "matching" required. */
public ObjectType CrossReferenceObjectType {get; set;}
public int ObjectId {get; set;}
public MatchStatus MustBeMetToAdvanceToStatus {get; set;}
public bool IsMet {get; set;}
}
public class CStatus {
public int Id {get; set;}
public string Name {get; set;}
public bool IsMatchable {get; set;}
}
public class C {
public int Id {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public virtual CStatus Status {get;set;}
public virtual IEnumerable<Check> Checks {get; set;}
C() {
this.Checks = new HashSet<Check>();
}
}
public class PStatus {
public int Id {get; set;}
public string Name {get; set;}
public bool IsMatchable {get; set;}
}
public class P {
public int Id {get; set;}
public string Title {get; set;}
public virtual PStatus Status { get; set;}
public virtual IEnumerable<Check> Checks {get; set;}
P() {
this.Checks = new HashSet<Check>();
}
}
public enum MatchStatus {
Initial = 0,
Step2 = 1,
Step3 = 2,
Final = 3,
Rejected = 4
}
public class Match {
public int Id {get; set;}
public MatchStatus Status {get; set;}
public virtual C C {get; set;}
public virtual P P {get; set;}
}
public class MatchCreationRequest {
public C C {get; set;}
public P P {get; set;}
}
public class MatchAdvanceRequest {
public Match Match {get; set;}
public MatchStatus StatusToAdvanceTo {get; set;}
}
public class Result<TIn, TOut> {
public bool Successful {get; set;}
public List<string> Messages {get; set;}
public TIn InValue {get; set;}
public TOut OutValue {get; set;}
public static Result<TIn, TOut> Failed<TIn>(TIn value, string message)
{
return Result<TIn, TOut>() {
InValue = value,
Messages = new List<string>() { message },
OutValue = null,
Successful = false
};
}
public Result<TIn, TOut> Succeeded<TIn, TOut>(TIn input, TOut output, string message)
{
return Result<TIn, TOut>() {
InValue = input,
Messages = new List<string>() { message },
OutValue = output,
Successful = true
};
}
}
public class MatchService {
public Result<MatchCreationRequest> CreateMatch(MatchCreationRequest request) {
if (!request.C.Status.IsMatchable) {
return Result<MatchCreationRequest, Match>.Failed(request, "C is not matchable because of its status.");
}
else if (!request.P.Status.IsMatchable) {
return Result<MatchCreationRequest, Match>.Failed(request, "P is not matchable because of its status.");
}
else if (request.C.Checks.Any(ccs => cs.ObjectType == ObjectType.C && !ccs.IsMet) {
return Result<MatchCreationRequest, Match>.Failed(request, "C is not matchable because its own Checks are not met.");
} else if (request.P.Checks.Any(pcs => pcs.ObjectType == ObjectType.P && !pcs.IsMet) {
return Result<MatchCreationRequest, Match>.Failed(request, "P is not matchable because its own Checks are not met.");
}
else if (request.P.Checks.Any(pcs => pcs.ObjectType == ObjectType.C && C.Checks.Any(ccs => !ccs.IsMet && ccs.CheckType == pcs.CheckType))) {
return Result<MatchCreationRequest, Match>.Failed(request, "P's Checks are not satisfied by C's Checks.");
}
else {
var newMatch = new Match() { C = c, P = p, Status = MatchStatus.Initial }
return Result<MatchCreationRequest, Match>.Succeeded(request, newMatch, "C and P passed all Checks.");
}
}
}
奖励:除了幼稚 "block Match" 状态之外,C 和 P 每个人都有一组检查。对于匹配的 C,某些检查必须为真,对于匹配的 P,某些检查必须为真,并且必须针对 P 的检查对 C 的某些检查进行交叉检查。这是我怀疑基于模型的地方使用 FsCheck 进行测试将带来巨大的收益,因为 (a) 它是添加到产品中的新功能的示例 (b) 我可能会编写测试(用户交互),例如:
- 创建
- 创建后,通过管道向前移动
- 向后移动(什么时候允许,什么时候不允许?例如:付费订单可能无法返回到采购批准步骤)
- Add/remove 东西(比如支票)在管道中间
- 如果我要求为相同的 C 和 P 创建匹配两次(例如,与 PLINQ 并发),我会创建重复项吗? (返回给用户的消息是什么?)
我正在努力的事情:
- 如何生成FsCheck的测试数据?我 认为 正确的方法是定义 Cs 和 Ps 的所有离散可能组合以创建匹配,并将它们作为我基于模型的测试的 "pre-conditions" post-条件是是否应该创建匹配,但是...
- 这真的是正确的方法吗?对于基于 randomized 属性 的测试工具来说,感觉过于确定。在这种情况下甚至使用 FsCheck 是否过度工程化?然后,就好像我有一个忽略种子值和 returns 确定性测试数据流的数据生成器。
- 在这一点上,FsCheck 生成器与仅使用 xUnit.net 和类似 AutoPOCO 的东西有什么不同吗?
如果您想生成确定性(包括详尽的)测试数据,那么 FsCheck 并不是一个很好的选择。基本假设之一是你的状态 space 太大以至于不可行,所以随机的,但引导生成能够找到更多的错误(很难证明这一点,但肯定有一些研究证实了这个假设。这并不是说它是所有情况下的最佳方法)。
根据您所写的内容,我假设 CreateMatch
方法是您想要测试其属性的方法;所以在那种情况下你应该尝试生成一个MatchCreationRequest
。由于生成器组合,这在你的情况下相当冗长(因为它们都是可变类型,没有基于反射的自动生成器)但也很容易 - 它总是相同的模式:
var genCStatus = from id in Arb.Generate<int>()
from name in Arb.Generate<string>()
from isMatchable in Arb.Generate<bool>()
select new CStatus { Id = id, Name = name, IsMatchable = isMatchable };
var genC = from status in genCStatus
...
select new C { ... }
一旦你有了这些,编写要测试的属性应该相对简单,尽管至少在这个例子中它们并不比实现本身简单得多。
一个例子是:
//check that if C or P are not matchable, the result is failed.
Prop.ForAll(genC.ToArbitrary(), genP.ToArbitrary(), (c, p) => {
var result = MatchService.CreateMatch(new MatchCreationRequest(c, p));
if (!c.IsMatchable || !p.IsMatchable) { Assert.IsFalse(result.Succesful); }
}).QuickCheckThrowOnFailure();
我正在尝试将范式转变为 FsCheck 和基于 属性 的随机测试。我有复杂的业务工作流程,其中的测试用例比我可能列举的要多,而且业务逻辑是一个不断添加新功能的移动目标。
背景:配对是企业资源规划 (ERP) 系统中非常常见的抽象概念。订单履行、供应链物流等
示例:给定一个 C 和一个 P,确定两者是否匹配。在任何给定的时间点,一些 Ps 是 never 可匹配的,而一些 C 是 never 可匹配的。每个人都有一个状态,表明他们是否可以被考虑参加比赛。
public enum ObjectType {
C = 0,
P = 1
}
public enum CheckType {
CertA = 0,
CertB = 1
}
public class Check {
public CheckType CheckType {get; set;}
public ObjectType ObjectType {get; set;}
/* If ObjectType == CrossReferenceObjectType, then it is assumed to be self-referential and there is no "matching" required. */
public ObjectType CrossReferenceObjectType {get; set;}
public int ObjectId {get; set;}
public MatchStatus MustBeMetToAdvanceToStatus {get; set;}
public bool IsMet {get; set;}
}
public class CStatus {
public int Id {get; set;}
public string Name {get; set;}
public bool IsMatchable {get; set;}
}
public class C {
public int Id {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
public virtual CStatus Status {get;set;}
public virtual IEnumerable<Check> Checks {get; set;}
C() {
this.Checks = new HashSet<Check>();
}
}
public class PStatus {
public int Id {get; set;}
public string Name {get; set;}
public bool IsMatchable {get; set;}
}
public class P {
public int Id {get; set;}
public string Title {get; set;}
public virtual PStatus Status { get; set;}
public virtual IEnumerable<Check> Checks {get; set;}
P() {
this.Checks = new HashSet<Check>();
}
}
public enum MatchStatus {
Initial = 0,
Step2 = 1,
Step3 = 2,
Final = 3,
Rejected = 4
}
public class Match {
public int Id {get; set;}
public MatchStatus Status {get; set;}
public virtual C C {get; set;}
public virtual P P {get; set;}
}
public class MatchCreationRequest {
public C C {get; set;}
public P P {get; set;}
}
public class MatchAdvanceRequest {
public Match Match {get; set;}
public MatchStatus StatusToAdvanceTo {get; set;}
}
public class Result<TIn, TOut> {
public bool Successful {get; set;}
public List<string> Messages {get; set;}
public TIn InValue {get; set;}
public TOut OutValue {get; set;}
public static Result<TIn, TOut> Failed<TIn>(TIn value, string message)
{
return Result<TIn, TOut>() {
InValue = value,
Messages = new List<string>() { message },
OutValue = null,
Successful = false
};
}
public Result<TIn, TOut> Succeeded<TIn, TOut>(TIn input, TOut output, string message)
{
return Result<TIn, TOut>() {
InValue = input,
Messages = new List<string>() { message },
OutValue = output,
Successful = true
};
}
}
public class MatchService {
public Result<MatchCreationRequest> CreateMatch(MatchCreationRequest request) {
if (!request.C.Status.IsMatchable) {
return Result<MatchCreationRequest, Match>.Failed(request, "C is not matchable because of its status.");
}
else if (!request.P.Status.IsMatchable) {
return Result<MatchCreationRequest, Match>.Failed(request, "P is not matchable because of its status.");
}
else if (request.C.Checks.Any(ccs => cs.ObjectType == ObjectType.C && !ccs.IsMet) {
return Result<MatchCreationRequest, Match>.Failed(request, "C is not matchable because its own Checks are not met.");
} else if (request.P.Checks.Any(pcs => pcs.ObjectType == ObjectType.P && !pcs.IsMet) {
return Result<MatchCreationRequest, Match>.Failed(request, "P is not matchable because its own Checks are not met.");
}
else if (request.P.Checks.Any(pcs => pcs.ObjectType == ObjectType.C && C.Checks.Any(ccs => !ccs.IsMet && ccs.CheckType == pcs.CheckType))) {
return Result<MatchCreationRequest, Match>.Failed(request, "P's Checks are not satisfied by C's Checks.");
}
else {
var newMatch = new Match() { C = c, P = p, Status = MatchStatus.Initial }
return Result<MatchCreationRequest, Match>.Succeeded(request, newMatch, "C and P passed all Checks.");
}
}
}
奖励:除了幼稚 "block Match" 状态之外,C 和 P 每个人都有一组检查。对于匹配的 C,某些检查必须为真,对于匹配的 P,某些检查必须为真,并且必须针对 P 的检查对 C 的某些检查进行交叉检查。这是我怀疑基于模型的地方使用 FsCheck 进行测试将带来巨大的收益,因为 (a) 它是添加到产品中的新功能的示例 (b) 我可能会编写测试(用户交互),例如:
- 创建
- 创建后,通过管道向前移动
- 向后移动(什么时候允许,什么时候不允许?例如:付费订单可能无法返回到采购批准步骤)
- Add/remove 东西(比如支票)在管道中间
- 如果我要求为相同的 C 和 P 创建匹配两次(例如,与 PLINQ 并发),我会创建重复项吗? (返回给用户的消息是什么?)
我正在努力的事情:
- 如何生成FsCheck的测试数据?我 认为 正确的方法是定义 Cs 和 Ps 的所有离散可能组合以创建匹配,并将它们作为我基于模型的测试的 "pre-conditions" post-条件是是否应该创建匹配,但是...
- 这真的是正确的方法吗?对于基于 randomized 属性 的测试工具来说,感觉过于确定。在这种情况下甚至使用 FsCheck 是否过度工程化?然后,就好像我有一个忽略种子值和 returns 确定性测试数据流的数据生成器。
- 在这一点上,FsCheck 生成器与仅使用 xUnit.net 和类似 AutoPOCO 的东西有什么不同吗?
如果您想生成确定性(包括详尽的)测试数据,那么 FsCheck 并不是一个很好的选择。基本假设之一是你的状态 space 太大以至于不可行,所以随机的,但引导生成能够找到更多的错误(很难证明这一点,但肯定有一些研究证实了这个假设。这并不是说它是所有情况下的最佳方法)。
根据您所写的内容,我假设 CreateMatch
方法是您想要测试其属性的方法;所以在那种情况下你应该尝试生成一个MatchCreationRequest
。由于生成器组合,这在你的情况下相当冗长(因为它们都是可变类型,没有基于反射的自动生成器)但也很容易 - 它总是相同的模式:
var genCStatus = from id in Arb.Generate<int>()
from name in Arb.Generate<string>()
from isMatchable in Arb.Generate<bool>()
select new CStatus { Id = id, Name = name, IsMatchable = isMatchable };
var genC = from status in genCStatus
...
select new C { ... }
一旦你有了这些,编写要测试的属性应该相对简单,尽管至少在这个例子中它们并不比实现本身简单得多。
一个例子是:
//check that if C or P are not matchable, the result is failed.
Prop.ForAll(genC.ToArbitrary(), genP.ToArbitrary(), (c, p) => {
var result = MatchService.CreateMatch(new MatchCreationRequest(c, p));
if (!c.IsMatchable || !p.IsMatchable) { Assert.IsFalse(result.Succesful); }
}).QuickCheckThrowOnFailure();