使用FsCheck进行测试的方法



我正试图将范式转移到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)我可以潜在地编写测试(用户交互),例如:

  1. 创建
  2. 创建后,通过管道向前移动
  3. 向后移动(何时允许与不允许?)例如:已付款订单可能无法移回购买批准步骤)
  4. 添加/删除东西(如检查),而在管道的中间
  5. 如果我要求为相同的C和P创建两次匹配(例如,与PLINQ并发),我会创建副本吗?(什么消息返回给用户?)

我正在挣扎的事情:

  1. 我应该如何为FsCheck生成测试数据?我认为正确的方法是定义创建匹配的所有离散的Cs和p的可能组合,并将这些作为基于模型的测试的"先决条件",并将后置条件作为是否应该创建匹配,但是…
  2. 这真的是正确的方法吗?对于一个随机的基于属性的测试工具来说,它感觉太确定了。在这种情况下使用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();

最新更新