使用 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) 我可能会编写测试(用户交互),例如:

  1. 创建
  2. 创建后,通过管道向前移动
  3. 向后移动(什么时候允许,什么时候不允许?例如:付费订单可能无法返回到采购批准步骤)
  4. Add/remove 东西(比如支票)在管道中间
  5. 如果我要求为相同的 C 和 P 创建匹配两次(例如,与 PLINQ 并发),我会创建重复项吗? (返回给用户的消息是什么?)

我正在努力的事情:

  1. 如何生成FsCheck的测试数据?我 认为 正确的方法是定义 Cs 和 Ps 的所有离散可能组合以创建匹配,并将它们作为我基于模型的测试的 "pre-conditions" post-条件是是否应该创建匹配,但是...
  2. 这真的是正确的方法吗?对于基于 randomized 属性 的测试工具来说,感觉过于确定。在这种情况下甚至使用 FsCheck 是否过度工程化?然后,就好像我有一个忽略种子值和 returns 确定性测试数据流的数据生成器。
  3. 在这一点上,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();