web-dev-qa-db-ja.com

自動固定および読み取り専用プロパティ

同じ非常に単純なエンティティの2つのバージョン(1つは読み取り専用プロパティを持つ)について考えてみましょう。

_public class Client
{
    public Guid Id { get; set; }

    public string Name { get; set; }
}
_

vs

_public class Client
{
    public Client(Guid id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public Guid Id { get; }

    public string Name { get; }
}
_

Autofixtureを使おうとすると、両方とも正しく動作し、期待どおりに動作します。 .with()メソッドを使用してパラメーターの1つを事前定義しようとすると、問題が始まります。

_var obj = this.fixture.Build<Client>().With(c => c.Name, "TEST").Build();
_

これはエラーをスローします

System.ArgumentException:プロパティ "Name"は読み取り専用です。

しかし、Autofixtureはコンストラクターの使用方法を知っているようです!そして、実際のBuild<>()メソッドは、Create()ではなくオブジェクトのインスタンスを作成するようです。 buildがビルダーを準備し、プロパティをセットアップし、Createがオブジェクトをインスタンス化する場合、読み取り専用プロパティで正しく機能します。

では、なぜこの(誤解を招く)戦略がここで使用されたのでしょうか?私は答えを見つけました ここ それはテストでフィードバックを増幅することであると述べていますが、特にパラメータのリストが広範囲にわたる場合、FromFactory()を使用することの有用性はわかりません。オブジェクトのインスタンス化をBuild()メソッドからCreate()メソッドに移動する方が直感的ではないでしょうか。

AutoFixtureは、実際、コンストラクター引数を作成し、コンストラクターを呼び出すことができます。特定のコンストラクター引数を制御する方法はFAQなので、それが唯一の質問だった場合は、 単一のコンストラクターパラメーターの値を指定する簡単な方法? の複製として閉じていました。

ただし、この投稿では、Build AP​​Iの動作の背後にある設計の選択についても質問しているので、ここで回答します。

2番目の例では、Nameは読み取り専用プロパティであり、読み取り専用プロパティの値を変更することはできません。これは.NET(および他のほとんどの言語)の一部であり、AutoFixtureの設計上の選択ではありません。

これについて完全に明確にしましょう:Nameはプロパティです。技術的には、クラスのコンストラクターとは何の関係もありません。

Nameがコンストラクターのname引数に関連付けられていると考えていると思います。これは、一方が他方を公開しているためですが、ソースコードがあるため、それしかわかりません。外部のオブザーバーがこれら2つが接続されていることを確認するための技術的に安全な方法はありません。 AutoFixtureなどの外部のオブザーバーは、そのような接続が存在することを推測しようとする可能性がありますが、保証はありません。

このようなコードを書くことは技術的に可能です:

public class Person
{
    public Person(string firstName, string lastName)
    {
        this.FirstName = lastName;
        this.LastName = firstName;
    }

    public string FirstName { get; }

    public string LastName { get; }
}

値が入れ替わっていても、これは問題なくコンパイルされます。 AutoFixtureはそのような問題を検出できません。

AutoFixtureにヒューリスティックを与えることは可能かもしれません。そこでは、読み取り専用プロパティを参照するときにBuild AP​​Iが「意味」を推測しようとしますが、私がまだプロジェクトの慈悲深い独裁者だったとき、私は不当に複雑な機能であると考えました。新しいメンテナがトピックについて異なって見える可能性があります。

一般的な見解として、私はBuildAPI全体を間違いだと考えています。過去数年間、私はAutoFixtureを使用してテストを作成しましたが、そのAPIを使用したことはありません。今日もプロジェクトを実行している場合は、そのAPIを非推奨にします。これは、人々がAutoFixtureを脆弱な方法で使用するようになるためです。

したがって、これは非常に明確な設計上の選択です。

5
Mark Seemann

私のクラスのほとんどは通常読み取り専用であるため、私もこれに苦労しています。 Json.Netなどの一部のライブラリは、命名規則を使用して、各プロパティに影響を与えるコンストラクター引数を理解します。

ISpecimenBuilderインターフェースを使用してプロパティをカスタマイズする方法は確かにあります。

public class OverridePropertyBuilder<T, TProp> : ISpecimenBuilder
{
    private readonly PropertyInfo _propertyInfo;
    private readonly TProp _value;

    public OverridePropertyBuilder(Expression<Func<T, TProp>> expr, TProp value)
    {
        _propertyInfo = (expr.Body as MemberExpression)?.Member as PropertyInfo ??
                        throw new InvalidOperationException("invalid property expression");
        _value = value;
    }

    public object Create(object request, ISpecimenContext context)
    {
        var pi = request as ParameterInfo;
        if (pi == null)
            return new NoSpecimen();

        var camelCase = Regex.Replace(_propertyInfo.Name, @"(\w)(.*)",
            m => m.Groups[1].Value.ToLower() + m.Groups[2]);

        if (pi.ParameterType != typeof(TProp) || pi.Name != camelCase)
            return new NoSpecimen();

        return _value;
    }
}

お気づきのように、これをBuild<>apiで使用しようとすると行き止まりになりました。そのため、自分で拡張メソッドを作成する必要がありました。

public class FixtureCustomization<T>
{
    public Fixture Fixture { get; }

    public FixtureCustomization(Fixture fixture)
    {
        Fixture = fixture;
    }

    public FixtureCustomization<T> With<TProp>(Expression<Func<T, TProp>> expr, TProp value)
    {
        Fixture.Customizations.Add(new OverridePropertyBuilder<T, TProp>(expr, value));
        return this;
    }

    public T Create() => Fixture.Create<T>();
}

public static class CompositionExt
{
    public static FixtureCustomization<T> For<T>(this Fixture fixture)
        => new FixtureCustomization<T>(fixture);
}

それは私がそのように使うことを可能にしました:

var obj = 
  new Fixture()
  .For<Client>()
  .With(x => x.Name, "TEST")
  .Create();

お役に立てれば

3
Fabio Marreco