0

我正在尝试将范式转变为 FsCheck 和基于随机属性的测试。我有复杂的业务工作流程,其中包含的测试用例比我可能列举的要多,而且业务逻辑是一个不断变化的目标,其中添加了新功能。

背景:匹配是企业资源计划(ERP)系统中非常常见的抽象。订单履行、供应链物流等

示例:给定一个 C 和一个 P,确定两者是否匹配。在任何给定的时间点,一些 P永远无法匹配,而一些 C永远无法匹配。每个人都有一个状态,表明他们是否可以考虑参加比赛。

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 和 Ps 的所有离散可能组合来创建匹配,并将它们作为我基于模型的测试的“前提条件”,后置条件是是否应该创建匹配,但是...
  2. 这真的是正确的方法吗?对于基于随机属性的测试工具来说,这感觉太确定了。在这种情况下甚至使用 FsCheck 是否过度设计?然后,就好像我有一个忽略种子值并返回确定性测试数据流的数据生成器。
  3. 在这一点上,FsCheck 生成器与仅使用 xUnit.net 和 AutoPOCO 之类的东西有什么不同吗?
4

1 回答 1

3

如果您想生成确定性(包括详尽的)测试数据,那么 FsCheck 并不是一个很好的选择。基本假设之一是您的状态空间太大而无法实现,因此随机,但引导生成能够找到更多错误(很难证明这一点,但肯定有一些研究证实了这一假设。那并不是说它在所有情况下都是最好的方法)。

我从您所写的内容中假设该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();
于 2015-09-20T08:46:57.257 回答