14

現在開発中のMVC3アプリケーションでリポジトリパターンを使用しています。私のリポジトリインターフェースは次のようになります。

public interface IRepository<TEntity> where TEntity : IdEntity
{
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Remove(TEntity entity);
    TEntity GetById(int id);
    IList<TEntity> GetAll();
    TEntity FindFirst(Expression<Func<TEntity, bool>> criteria);
    IList<TEntity> Find(Expression<Func<TEntity, bool>> criteria);
}

多くの場合、サービスクラスでメソッドをコーディングするときは、FindFirstandFindメソッドを使用しています。ご覧のとおり、どちらもlinq式を入力として受け取ります。私が知りたいのは、NSubstituteを使用して、コードでテストする特定の式を指定できる方法があるかどうかです。

それで、これは私が言及したリポジトリメソッドの1つの使用法を説明するサービスメソッドの例です:

public IList<InvoiceDTO> GetUnprocessedInvoices()
{
    try
    {
        var invoices = _invoiceRepository.Find(i => !i.IsProcessed && i.IsConfirmed);
        var dtoInvoices = Mapper.Map<IList<Invoice>, IList<InvoiceDTO>>(invoices);
        return dtoInvoices;
    }
    catch (Exception ex)
    {
        throw new Exception(string.Format("Failed to get unprocessed invoices: {0}", ex.Message), ex);
    }
}

それで、NSubtituteを使用して、特定のlamda式をテストできる方法はありi => !i.IsProcessed && i.IsConfirmed ますか?

任意のガイダンスをいただければ幸いです。

4

4 に答える 4

20

非常に短い答えはノーです。NSubstituteには、特定の式のテストを容易にするためのビルドはありません。

はるかに長い答えは、試すことができるいくつかのオプションがあり、それらのほとんどは、テスト対象のクラスでLINQを直接使用しないようにすることです。完全なコンテキストがわからないため、これらのいずれかが優れたアイデアであるかどうかはわかりませんが、ここで使用できる情報があることを願っています。次の例では、コードサンプルを少し小さくするためにマッパーステップを削除しました。

最初のオプションは、式が期待しているものと同じ参照であることを確認できるようにすることです。つまり、テスト対象のコードで式を直接作成することはできなくなります。例えば:

//Class under test uses:
_invoiceRepository.Find(Queries.UnprocessedConfirmedOrders)

[Test]
public void TestUnprocessedInvoices()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Queries.UnprocessedConfirmedOrders).Returns(expectedResults);
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
}

式を静的クエリクラスにダンプしましたが、ファクトリを使用してより適切にカプセル化することができます。使用される実際の式への参照があるため、戻り値を設定し、呼び出しが通常どおり受信されたことを確認できます。式を単独でテストすることもできます。

2番目のオプションは、仕様パターンを使用してこれを少し進めます。次のメンバーをIRepositoryインターフェースに追加し、ISpecificationを導入するとします。

public interface IRepository<TEntity> where TEntity : IdEntity
{
   /* ...snip... */
    IList<TEntity> Find(ISpecification<TEntity> query);
}

public interface ISpecification<T> { bool Matches(T item);  }

次に、次のようにテストできます。

//Class under test now uses:
_invoiceRepository.Find(new UnprocessedConfirmedOrdersQuery());

[Test]
public void TestUnprocessedInvoicesUsingSpecification()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Arg.Any<UnprocessedConfirmedOrdersQuery>()).Returns(expectedResults);
    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
}

繰り返しになりますが、このクエリを個別にテストして、思ったとおりに動作することを確認できます。

3番目のオプションは、使用された引数をキャッチして直接テストすることです。これは少し厄介ですが、機能します:

[Test]
public void TestUnprocessedInvoicesByCatchingExpression()
{
    Expression<Func<InvoiceDTO, bool>> queryUsed = null;
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository
        .Find(i => true)
        .ReturnsForAnyArgs(x =>
        {
            queryUsed = (Expression<Func<InvoiceDTO, bool>>)x[0];
            return expectedResults;
        });

    Assert.That(_sut.GetUnprocessedInvoices(), Is.SameAs(expectedResults));
    AssertQueryPassesFor(queryUsed, new InvoiceDTO { IsProcessed = false, IsConfirmed = true });
    AssertQueryFailsFor(queryUsed, new InvoiceDTO { IsProcessed = true, IsConfirmed = true });
}

(これは、将来のNSubstituteバージョンで少し簡単になることを願っています)

4番目のオプションは、式ツリーを比較できるコードを検索/借用/書き込み/盗用し、述語を使用してそこで式ツリーを比較するNSubstituteのArg.Is(...)を使用することです。

5番目のオプションは、その程度まで単体テストを行わず、実際のInvoiceRepositoryを使用して統合テストを行うことです。何が起こっているのかを心配するのではなく、必要な実際の動作を確認してみてください。

私の一般的なアドバイスは、テストする必要があるものを正確に調べ、それらのテストを最も簡単に作成する方法を検討することです。式とそれが通過するという事実の両方を何らかの方法でテストする必要があり、テストは単体テストである必要はないことを忘れないでください。また、現在のIRepositoryインターフェイスがあなたの生活を楽にしているかどうかを検討する価値があるかもしれません。必要なテストを作成してみて、そのテスト容易性をサポートするためにどのような設計を実行できるかを確認できます

お役に立てれば。

于 2011-04-14T05:16:13.833 に答える
8

NSubstituteでラムダ式を使用して特定の値を返す方法を見つけようとしていたときに、この質問に出くわしました。ただし、私のユースケースでは、実際にlinqクエリに何が渡されるかは気にせず、NSubstituteのモックインターフェイスでlinqクエリの値を返す方法を共有したいと思いました。

したがって、上記の例を使用します

[Test]
public void TestUnprocessedInvoices()
{
    IList<InvoiceDTO> expectedResults = new List<InvoiceDTO>();
    _invoiceRepository.Find(Arg.Any<Expression<Func<Invoice, bool>>>()).Returns(expectedResults);
}
于 2011-06-13T18:43:30.643 に答える
4

ラムダ式が等しいかどうかを比較することで、これを行う方法があります。LambdaCompareクラスの例を示す、関連する質問に対する非常に人気のある回答がここに書かれています。

次に、このLambdaCompareを使用して、モックセットアップで式またはラムダが等しいかどうかを確認できます。

var mockRepository = Substitute.For<IRepository>();
mockRepository.Find(Arg.Is<Expression<Func<Invoice, bool>>>(expr =>
                    LambdaCompare.Eq(expr, i => !i.IsProcessed && i.IsConfirmed))
              .Returns(..etc..)

モックリポジトリ.Find()が式で呼び出された場合にのみ、i => !i.IsProcessed && i.IsConfirmedで指定されたものが返されます.Returns()

于 2019-03-28T18:22:52.897 に答える
3

リポジトリインターフェイスでの使用をあきらめExpression<Func<T,bool>>たくなかったので、この1つの特定のモックをプログラミングする代わりに(NSubstituteがサポートしていなかったため)、リポジトリインターフェイスを実装するプライベートクラスをテストフィクスチャ内に作成しました。テストで使用する式関連のメソッドのみ。いつものようにNSubstituteを使用して他のすべての依存関係をモックすることができましたが、この同じリポジトリをいくつかの異なるテストに使用して、実際には異なる入力から異なる結果を得ることができました。

public class SomeFixture
{
    private readonly IRepository<SomeEntity> entityRepository;
    private readonly IRepository<SomeThing> thingRepository;

    public SomeFixture()
    {
        var entities = new List<SomeEntity>
        {
            BuildEntityForThing(1),
            BuildEntityForThing(1),
            BuildEntityForThing(1),
            BuildEntityForThing(2),
        };
        entityRepository = new FakeRepository(entities);

        thingRepository = Substitute.For<IRepository<SomeThing>>();
        thingRepository.GetById(1).Returns(BuildThing(1));
        thingRepository.GetById(2).Returns(BuildThing(2));
    }

    public void SomeTest()
    {
        var classUnderTest = new SomeClass(thingRepository, entityRepository);

        Assert.AreEqual(classUnderTest.FetchEntitiesForThing(1).Count, 3);
    }

    private void SomeOtherTest()
    {
        var classUnderTest = new SomeClass(thingRepository, entityRepository);

        Assert.AreEqual(classUnderTest.FetchEntitiesForThing(2).Count, 1);
    }

    private class FakeRepository : IRepository<SomeEntity>
    {
        private readonly List<SomeEntity> items;

        public FakeRepository(List<SomeEntity> items)
        {
            this.items = items;
        }

        IList<TEntity> Find(Expression<Func<SomeEntity, bool>> criteria)
        {
            // For these purposes, ignore possible inconsistencies 
            // between Linq and SQL when executing expressions
            return items.Where(criteria.Compile()).ToList();
        }

        // Other unimplemented methods from IRepository ...
        void Add(SomeEntity entity)
        {
            throw new NotImplementedException();
        }
    }
}
于 2014-09-30T18:22:55.713 に答える