Ktouth Brand. on Web

け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト



[2014年08月03日]

NUnit で独自の(ちょっと複雑な)制約クラスを追加する

2014年08月05日 19:12更新 筆者:K.Ktouth

NUnit でちょっと複雑な制約クラスを見よう見まねで作ってみたので、かるく自分メモ。
単純に自分だけで制約が完結する場合は Constraint 抽象クラスから派生していくつかのメソッドを実装するだけで済みますが、例えば Has.Property(xxx) メソッドのように「プロパティがあるかどうかを検証」「後に続く制約がある場合にそのプロパティの値を渡す」みたいな処理を実装したい場合、 Constraint からだけでは間に合わない……というか検証そのものとメソッドチェーンの構築は別クラスで実装すると言うことらしいです。
ソースコードの PropertyConstraint や PropertyOperator、さらには ConstraintExpression に Is や Has などのクラスも参考にすることになります。

今回は Catel.ModelBase の Validation を実行してフィールドエラーの検証をする制約クラスを実装してみました。

# あ、先に書いておくと。
# NUnit 安定版の 2.6.x と GitHub で開発されている最新版の 2.9.x は制約クラスの設計が変わっていますので注意!

(以下、ソースコード)

ValidateConstraint 制約クラス

public class ValidateConstraint : PrefixConstraint
{
  private readonly string _Name = null;
  private readonly bool _CheckErrors;
  private readonly bool _CheckWarnings;
  private readonly bool _ForceValidate;
  private readonly List<IValidationResult> _Errors = new List<IValidationResult>();
  private readonly List<IValidationResult> _Warnings = new List<IValidationResult>();

  public ValidateConstraint(string name, bool checkErrors, bool checkWarnings, bool forceValidate, IResolveConstraint baseConstraint)
    : this(checkErrors, checkWarnings, forceValidate, baseConstraint)
  {
    Argument.IsNotNullOrEmpty("name", name);
    _Name = name;
  }
  public ValidateConstraint(bool checkErrors, bool checkWarnings, bool forceValidate, IResolveConstraint baseConstraint)
    : base(baseConstraint)
  {
    _CheckErrors = checkErrors;
    _CheckWarnings = checkWarnings;
    _ForceValidate = forceValidate;
  }

  public override bool Matches(object actual)
  {
    Argument.IsNotNull("actual", actual);
    this.actual = actual;

    var actualType = (actual as Type) ?? actual.GetType();
    ModelBase actualModel = actual as ModelBase;
    if (actualModel == null)
    {
      var klass = actualType != null ? actualType.FullName : "null";
      throw new ArgumentException(string.Format("actual<{0}> is not inherited <Catel.ModelBase>", klass), "actual");
    }

    _Errors.Clear();
    _Warnings.Clear();

    if (_Name != null)
    {
      PropertyInfo property = actualType.GetProperty(_Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);
      if (property == null) { throw new ArgumentException(string.Format("Property {0} was not found", _Name), "_Name"); }

      actualModel.Validate(_ForceValidate);
      if (_CheckErrors && actualModel.HasErrors) { _Errors.AddRange(actualModel.ValidationContext.GetFieldErrors(_Name)); }
      if (_CheckWarnings && actualModel.HasWarnings) { _Warnings.AddRange(actualModel.ValidationContext.GetFieldWarnings(_Name)); }
    }
    else
    {
      actualModel.Validate(_ForceValidate);
      if (_CheckErrors && actualModel.HasErrors) { _Errors.AddRange(actualModel.ValidationContext.GetFieldErrors()); }
      if (_CheckWarnings && actualModel.HasWarnings) { _Warnings.AddRange(actualModel.ValidationContext.GetFieldWarnings()); }
    }
    return baseConstraint.Matches(!(_Errors.Any() || _Warnings.Any()));
  }

  public override void WriteDescriptionTo(MessageWriter writer)
  {
    writer.WritePredicate(_Name != null ? "validation of property " + _Name : "validation of object");
    if (baseConstraint != null)
    {
      if (baseConstraint is EqualConstraint) { writer.WritePredicate("equal to"); }
      baseConstraint.WriteDescriptionTo(writer);
    }
  }

  public override void WriteActualValueTo(MessageWriter writer)
  {
    if (_CheckErrors) { writer.Write("{0} errors", _Errors.Count); }
    if (_CheckWarnings)
    {
      if (_CheckErrors) { writer.Write(" and "); }
      writer.Write("{0} warnings", _Warnings.Count);
    }
  }

  protected override string GetStringRepresentation()
  {
    return _Name != null
      ? string.Format("validate<property {0} {1}>", _Name, baseConstraint)
      : string.Format("validate<{0}>", baseConstraint);
  }
}

まず、基底クラスは PrefixConstraint を使用しています。コイツは「次(実際に検証する)の制限インスタンス」を保持する機能があります。つまり Match クラスで検証に必要な前処理を行い、この baseConstraint にその結果を渡して検証してもらうという形になります。今回は Catel.ModelBase#Validate を実行し、その検証結果を必要に応じて絞り込み、エラー(ないし警告)があるかどうかを baseConstraint に渡しています。
また検証する値を変更していますので Description だけで無く Actual に関しても適当にメッセージを書き換えています。

ValidateOperator

次はメソッドチェーンを構築するために、制約クラスを実際に還元するオペレータクラスです。

public class ValidateOperator : SelfResolvingOperator
{
  public ValidateOperator(string name, bool checkErrors, bool checkWarnings, bool force)
    : this(checkErrors, checkWarnings, force)
  {
    Argument.IsNotNullOrEmpty("name", name);
    Name = name;
  }
  public ValidateOperator(bool checkErrors, bool checkWarnings, bool force)
    : base()
  {
    Name = null;
    CheckErrors = checkErrors;
    CheckWarnings = checkWarnings;
    Force = force;

    base.left_precedence = base.right_precedence = 1;
  }

  public string Name { get; private set; }
  public bool CheckErrors { get; private set; }
  public bool CheckWarnings { get; private set; }
  public bool Force { get; private set; }

  public override void Reduce(ConstraintBuilder.ConstraintStack stack)
  {
    var right = (RightContext == null) || (RightContext is BinaryOperator) ? new TrueConstraint() : stack.Pop();
    var constraint = Name != null
      ? new ValidateConstraint(Name, CheckErrors, CheckWarnings, Force, right)
      : new ValidateConstraint(CheckErrors, CheckWarnings, Force, right);
    stack.Push(constraint);
  }
}

主に作業をやっているのは Reduce メソッド。
次の制約クラスインスタンスを取得し、それを用いて制約クラスを作っています。作った制約クラスはスタックに突っ込んでいます。

ConstraintExpressionEx、Is

次はメソッドチェーンを実際に文法として書くために必要なクラスの定義です。

public class ConstraintExpressionEx : ConstraintExpression
{
  public ResolvableConstraintExpression Validate(bool force = true)
  {
    return this.Append(new ValidateOperator(true, true, force));
  }
  public ResolvableConstraintExpression Validate(string name, bool force = true)
  {
    return this.Append(new ValidateOperator(name, true, true, force));
  }
  public ResolvableConstraintExpression ValidateError(bool force = true)
  {
    return this.Append(new ValidateOperator(true, false, force));
  }
  public ResolvableConstraintExpression ValidateError(string name, bool force = true)
  {
    return this.Append(new ValidateOperator(name, true, false, force));
  }
  public ResolvableConstraintExpression ValidateWarning(bool force = true)
  {
    return this.Append(new ValidateOperator(false, true, force));
  }
  public ResolvableConstraintExpression ValidateWarning(string name, bool force = true)
  {
    return this.Append(new ValidateOperator(name, false, true, force));
  }
}

要は標準の ConstraintExpression に適当にメソッドを追加しただけです。これだけだと And 演算子などでは使えず、ようはメソッドチェーンの最初でしか Validate 関連メソッドは実行できません。要練り込み。

class Is : NUnit.Framework.Is
{
  public static ResolvableConstraintExpression Validate(bool force = true)
  {
    return new ConstraintExpressionEx().Validate(force);
  }
  public static ResolvableConstraintExpression Validate(string name, bool force = true)
  {
    return new ConstraintExpressionEx().Validate(name, force);
  }
  public static ResolvableConstraintExpression ValidateError(bool force = true)
  {
    return new ConstraintExpressionEx().ValidateError(force);
  }
  public static ResolvableConstraintExpression ValidateError(string name, bool force = true)
  {
    return new ConstraintExpressionEx().ValidateError(name, force);
  }
  public static ResolvableConstraintExpression ValidateWarning(bool force = true)
  {
    return new ConstraintExpressionEx().ValidateWarning(force);
  }
  public static ResolvableConstraintExpression ValidateWarning(string name, bool force = true)
  {
    return new ConstraintExpressionEx().ValidateWarning(name, force);
  }
}

さらに上記クラスを呼び出す実装。
通常は静的クラスとして実装するところを Is、Has に関しては通常クラスとして宣言してあります。つまり、自前でクラスを派生して静的メソッドを追加することを想定しての実装になっているわけです。標準の制約もそのまま使えるので便利ですね♪
もちろん NUnit.Framework.Is と名前が競合するでしょうから using を指定する順番や優先度をきちんと指定してやる必要があります。
具体的には……

using NUnit.Framework;
using System.ComponentModel;
.....

namespace FooBar.Test
{
  using FooBar.Test.Support; // ここに Is の派生クラスを定義している

  [TestFixture]
  class FooBarTest
  {
    [TestCase]
    public void FooBarテスト()
    {
      Assert.That(new FooBar(), Is.Validate("Name").True);
    }
  }
}

こんな感じになります。
ざっと駆け足ですがこうすることで自前の制約クラスがシームレスに使えて読みやすいコードになると思われます。
実際には ResolvableConstraintExpression から自前の制約クラスを呼び出すための拡張メソッドを定義する静的クラスを定義するなどもうちょっとすることがあるかな?

本日のリンク元
アンテナ
その他のリンク元
検索