我正试图将范式转移到FsCheck和基于随机属性的测试。我有复杂的业务工作流,它有比我可能列举的更多的测试用例,并且业务逻辑是添加新特性的移动目标。
背景:配对是企业资源规划(ERP)系统中非常常见的抽象。订单履行、供应链物流等
示例:给定C和p,确定两者是否匹配。在任意给定的时间点,一些p是永远不能匹配的,一些c是永远不能匹配的。每个都有一个Status,表示是否可以考虑将它们作为匹配。
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.");
}
}
}
奖励:除了简单的"方块匹配"状态外,C和p各有一组检查。对于匹配的C,有些检查必须为真,对于匹配的P,有些检查必须为真,对于C,有些检查必须与P的检查进行交叉检查。这就是我怀疑使用FsCheck进行基于模型的测试将带来巨大收益的地方,因为(a)它是添加到产品中的新功能的示例(b)我可以潜在地编写测试(用户交互),例如:
- 创建
- 创建后,通过管道向前移动
- 向后移动(何时允许与不允许?)例如:已付款订单可能无法移回购买批准步骤)
- 添加/删除东西(如检查),而在管道的中间
- 如果我要求为相同的C和P创建两次匹配(例如,与PLINQ并发),我会创建副本吗?(什么消息返回给用户?)
我正在挣扎的事情:
- 我应该如何为FsCheck生成测试数据?我认为正确的方法是定义创建匹配的所有离散的Cs和p的可能组合,并将这些作为基于模型的测试的"先决条件",并将后置条件作为是否应该创建匹配,但是…
- 这真的是正确的方法吗?对于一个随机的基于属性的测试工具来说,它感觉太确定了。在这种情况下使用FsCheck是不是过于工程化了?然后,这几乎就好像我有一个数据生成器,它忽略种子值并返回测试数据的确定性流。在这一点上,FsCheck生成器与仅仅使用xUnit.net和像AutoPOCO这样的东西有什么不同吗?
如果您想要生成确定性(包括详尽的)测试数据,那么FsCheck并不是一个很好的选择。其中一个基本假设是,你的状态空间太大而不可行,所以是随机的,但引导生成能够找到更多的bug(很难证明这一点,但肯定有一些研究证实了这个假设)。这并不是说它在所有情况下都是最好的方法。
我从你写的东西中假设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();