web-dev-qa-db-ja.com

Entity FrameworkSqlQuery呼び出しをMoqする方法

これを使用して、Moqを使用してエンティティフレームワークからDbSetをモックすることができました link

ただし、SqlQueryの呼び出しをモックする方法を知りたいと思います。これが可能かどうか、またはどの「クエリ」が呼び出されているかを知っているモックされたデータベースコンテキストにどのように依存しているかはわかりません。

以下は私がモックしようとしているものです。

var myObjects = DbContext.Database
    .SqlQuery<MyObject>("exec [dbo].[my_sproc] {0}", "some_value")
    .ToList();

この例をあざける方法がわからなかったので、私は現在何も試していません。

DbSetのモックは以下のとおりです。繰り返しになりますが、DbSetMyObjectを正しくモックで返すことができますが、現在、のリストを返すSqlQueryをモックしようとしています。 MyObjectの。

var dbContext = new Mock<MyDbContext>();
dbContext.Setup(m => m.MyObjects).Returns(mockObjects.Object);

dbContext.Setup(m => m.Database.SqlQuery... something along these lines
19
David

_Database.SqlQuery<T>_ は仮想としてマークされていませんが、 _Set<T>.SqlQuery_ は仮想としてマークされています。

_Database.SqlQuery<T>_ ドキュメントに基づく

返されるオブジェクトのタイプがエンティティタイプであっても、このクエリの結果がコンテキストによって追跡されることはありません。 'SqlQuery(String、Object [])' メソッドを使用して、コンテキストによって追跡されるエンティティを返します。

および _Set<T>.SqlQuery_ ドキュメント

デフォルトでは、返されるエンティティはコンテキストによって追跡されます。これは、返されたDbRawSqlQueryでAsNoTrackingを呼び出すことで変更できます。

その場合、Database.SqlQuery<T>(String, Object[])Set<T>.SqlQuery(String, Object[]).AsNoTracking()と同等である必要があります(TがEFエンティティであり、DTO/VMではない場合のみ)。

したがって、実装を次のように置き換えることができる場合:

_var myObjects = DbContext
    .Set<MyObject>()
    .SqlQuery("exec [dbo].[my_sproc] {0}", "some_value")
    .AsNoTracking()
    .ToList();
_

次のようにモックできます

_var list = new[] 
{ 
    new MyObject { Property = "some_value" },
    new MyObject { Property = "some_value" },
    new MyObject { Property = "another_value" }
};

var setMock = new Mock<DbSet<MyObject>>();
setMock.Setup(m => m.SqlQuery(It.IsAny<string>(), It.IsAny<object[]>()))
    .Returns<string, object[]>((sql, param) => 
    {
        // Filters by property.
        var filteredList = param.Length == 1 
            ? list.Where(x => x.Property == param[0] as string) 
            : list;
        var sqlQueryMock = new Mock<DbSqlQuery<MyObject>>();
        sqlQueryMock.Setup(m => m.AsNoTracking())
            .Returns(sqlQueryMock.Object);
        sqlQueryMock.Setup(m => m.GetEnumerator())
            .Returns(filteredList.GetEnumerator());
        return sqlQueryMock.Object;
    });

var contextMock = new Mock<MyDbContext>();
contextMock.Setup(m => m.Set<MyObject>()).Returns(setMock.Object);
_
20
Yuliam Chandra

単体テストでオーバーライドできる仮想メソッドをデータベースコンテキストに追加できます。

public partial class MyDatabaseContext : DbContext
{
    /// <summary>
    /// Allows you to override queries that use the Database property
    /// </summary>
    public virtual List<T> SqlQueryVirtual<T>(string query)
    {
        return this.Database.SqlQuery<T>(query).ToList();
    }
}
10
MichaelC

Database プロパティと SqlQuery メソッドはvirtualとしてマークされていないため、 できませんmocked (Moqを使用します。これを説明できる 別のライブラリ を使用できますが、それはあなたが望むよりも慣性が大きい可能性があります)。

これを回避するには、データベースのクエリ全体をヘルパークラスでラップするなど、何らかの抽象化を使用する必要があります。

public interface IQueryHelper
{
    IList<MyObject> DoYourQuery(string value);
}

public class QueryHelper : IQueryHelper
{
    readonly MyDbContext myDbContext;

    public QueryHelper(MyDbContext myDbContext)
    {
        this.myDbContext = myDbContext;
    }

    public IList<MyObject> DoYourQuery(string value)
    {
        return myDbContext.Database.SqlQuery<MyObject>("exec [dbo].[my_sproc] {0}", value).ToList();
    }
}

これで、テストするメソッドは次のようになります。

public void YourMethod()
{
    var myObjects = queryHelper.DoYourQuery("some_value");
}

次に、テストしているクラスのコンストラクターにIQueryHelperを挿入し、それをモックします。

DoYourQueryのテストカバレッジが欠落することになりますが、クエリは 非常に単純で、明らかに欠陥はありません です。

9
Patrick Quirk

誰かがこれに出くわした場合。私はいくつかのアプローチでこれを解決しました。これに対処するためのちょうど別の方法。

  1. 私のコンテキストはインターフェースを介して抽象化されます。私はいくつかの方法だけが必要です:

    public interface IDatabaseContext
    {
        DbSet<T> Set<T>() where T : class;
        DbEntityEntry<T> Entry<T>(T entity) where T : class;
        int SaveChanges();
        Task<int> SaveChangesAsync();
        void AddOrUpdateEntity<TEntity>(params TEntity[] entities) where TEntity : class;
    

    }

  2. 私のデータベースアクセスはすべて非同期メソッドを介して行われます。これは、モックを作成しようとすると、まったく新しい一連の問題を引き起こします。幸いなことに、回答済みです ここ 発生する例外は、IDbAsyncEnumerableのモックが欠落していることに関連しています。提供されたソリューションを使用する-期待されるすべてのプロパティをモックするMock>オブジェクトを返すヘルパーができるように、もう少し拡張しました。

    public static Mock<DbSqlQuery<TEntity>> CreateDbSqlQuery<TEntity>(IList<TEntity> data)
        where TEntity : class, new()
    {
        var source = data.AsQueryable();
        var mock = new Mock<DbSqlQuery<TEntity>>() {CallBase = true};
        mock.As<IQueryable<TEntity>>().Setup(m => m.Expression).Returns(source.Expression);
        mock.As<IQueryable<TEntity>>().Setup(m => m.ElementType).Returns(source.ElementType);
        mock.As<IQueryable<TEntity>>().Setup(m => m.GetEnumerator()).Returns(source.GetEnumerator());
        mock.As<IQueryable<TEntity>>().Setup(m => m.Provider).Returns(new TestDbAsyncQueryProvider<TEntity>(source.Provider));
        mock.As<IDbAsyncEnumerable<TEntity>>().Setup(m => m.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<TEntity>(data.GetEnumerator()));
        mock.As<IDbSet<TEntity>>().Setup(m => m.Create()).Returns(new TEntity());
        mock.As<IDbSet<TEntity>>().Setup(m => m.Add(It.IsAny<TEntity>())).Returns<TEntity>(i => { data.Add(i); return i; });
        mock.As<IDbSet<TEntity>>().Setup(m => m.Remove(It.IsAny<TEntity>())).Returns<TEntity>(i => { data.Remove(i); return i; });
        return mock;
    }
    
  3. 最後に、@ Yulium Chandraが提供するソリューションを使用して、モックされたコンテキストを使用した生のSQLのテストは次のようになります。

        public Mock<DbSet<TestModel>> MockDbSet { get; }
    
        ....
    
        MockDbSet.Setup(x => x.SqlQuery(It.IsAny<string>))
              .Returns<string,object[]>
              ((sql, param) => 
              {
                    var sqlQueryMock = MockHelper.CreateDbSqlQuery(Models);
    
                    sqlQueryMock.Setup(x => x.AsNoTracking())
                      .Returns(sqlQueryMock.Object);
    
                    return sqlQueryMock.Object;
                });
    
1
JBenn