現在、EntityFrameworkを介して実行されているクエリでいくつかの単体テストを実行しようとしています。クエリ自体はライブバージョンでは問題なく実行されますが、単体テストは常に失敗します。
これをDbFunctions.TruncateTimeの使用法に絞り込みましたが、ライブサーバーで何が起こっているかを単体テストに反映させる方法がわかりません。
これが私が使用している方法です:
public System.Data.DataTable GetLinkedUsers(int parentUserId)
{
var today = DateTime.Now.Date;
var query = from up in DB.par_UserPlacement
where up.MentorId == mentorUserId
&& DbFunctions.TruncateTime(today) >= DbFunctions.TruncateTime(up.StartDate)
&& DbFunctions.TruncateTime(today) <= DbFunctions.TruncateTime(up.EndDate)
select new
{
up.UserPlacementId,
up.Users.UserId,
up.Users.FirstName,
up.Users.LastName,
up.Placements.PlacementId,
up.Placements.PlacementName,
up.StartDate,
up.EndDate,
};
query = query.OrderBy(up => up.EndDate);
return this.RunQueryToDataTable(query);
}
DbFunctionsが含まれている行をコメントアウトすると、すべてのテストに合格します(特定の日付の有効な結果のみが実行されることを確認しているものを除く)。
これらのテストで使用するDbFunctions.TruncateTimeのモックバージョンを提供する方法はありますか?基本的にはDatetime.Dateを返すだけですが、EFクエリでは使用できません。
編集:日付チェックを使用して失敗したテストは次のとおりです。
[TestMethod]
public void CanOnlyGetCurrentLinkedUsers()
{
var up = new List<par_UserPlacement>
{
this.UserPlacementFactory(1, 2, 1), // Create a user placement that is current
this.UserPlacementFactory(1, 3, 2, false) // Create a user placement that is not current
}.AsQueryable();
var set = DLTestHelper.GetMockSet<par_UserPlacement>(up);
var context = DLTestHelper.Context;
context.Setup(c => c.par_UserPlacement).Returns(set.Object);
var getter = DLTestHelper.New<LinqUserGetLinkedUsersForParentUser>(context.Object);
var output = getter.GetLinkedUsers(1);
var users = new List<User>();
output.ProcessDataTable((DataRow row) => students.Add(new UserStudent(row)));
Assert.AreEqual(1, users.Count);
Assert.AreEqual(2, users[0].UserId);
}
編集2:これは、問題のテストからのメッセージとデバッグトレースです。
Test Result: Failed
Message: Assert.AreEqual failed. Expected:<1>. Actual:<0>
Debug Trace: This function can only be invoked from LINQ to Entities
私が読んだところによると、これは、ユニットテストのためにこの場所で使用できるこのメソッドのLINQ to Entities実装がないためです(SQLサーバーにクエリを実行しているため)。
みんなの助けに感謝し、qujckが言及したシムを読んだ後、私はうまくいった解決策を見つけることができました。 EntityFrameworkの偽のアセンブリを追加した後、これらのテストを次のように変更することで修正できました。
[TestMethod]
public void CanOnlyGetCurrentLinkedUsers()
{
using (ShimsContext.Create())
{
System.Data.Entity.Fakes.ShimDbFunctions.TruncateTimeNullableOfDateTime =
(DateTime? input) =>
{
return input.HasValue ? (DateTime?)input.Value.Date : null;
};
var up = new List<par_UserPlacement>
{
this.UserPlacementFactory(1, 2, 1), // Create a user placement that is current
this.UserPlacementFactory(1, 3, 2, false) // Create a user placement that is not current
}.AsQueryable();
var set = DLTestHelper.GetMockSet<par_UserPlacement>(up);
var context = DLTestHelper.Context;
context.Setup(c => c.par_UserPlacement).Returns(set.Object);
var getter = DLTestHelper.New<LinqUserGetLinkedUsersForParentUser>(context.Object);
var output = getter.GetLinkedUsers(1);
}
var users = new List<User>();
output.ProcessDataTable((DataRow row) => users.Add(new User(row)));
Assert.AreEqual(1, users.Count);
Assert.AreEqual(2, users[0].UserId);
}
ゲームに遅れていることはわかっていますが、非常に簡単な修正は、DbFunction属性を使用する独自のメソッドを作成することです。次に、DbFunctions.TruncateTimeの代わりにその関数を使用します。
[DbFunction("Edm", "TruncateTime")]
public static DateTime? TruncateTime(DateTime? dateValue)
{
return dateValue?.Date;
}
この関数を使用すると、Linq toEntitiesで使用される場合はEDMTruncateTimeメソッドが実行され、それ以外の場合は提供されたコードが実行されます。
この答えをチェックしてください: https://stackoverflow.com/a/14975425/1509728
正直に言うと、私はその答えに完全に同意し、EFクエリはデータベースに対してテストされ、アプリケーションコードのみがMoqでテストされるという原則に従います。
上記のクエリでEFクエリをテストするためにMoqを使用するための洗練されたソリューションはないようですが、そこにはいくつかのハッキーなアイデアがあります。たとえば これ とそれに続く答え。どちらもあなたのために働くことができるようです。
クエリをテストする別のアプローチは、私が取り組んだ別のプロジェクトに実装することです。VSのすぐに使える単体テストを使用して、各クエリ(これも独自のメソッドにリファクタリング)テストをトランザクションスコープにラップします。次に、プロジェクトのテストフレームワークが、偽のデータをデータベースに手動で入力する処理を行い、クエリがこの偽のデータをフィルタリングしようとします。最後に、トランザクションが完了することはないため、ロールバックされます。 トランザクションスコープの性質上、これは多くのプロジェクトにとって理想的なシナリオではない可能性があります。おそらく本番環境ではありません。
それ以外の場合、モック機能を継続する必要がある場合は、他のモックフレームワークを検討することをお勧めします。
それを行う方法があります。 ビジネスロジックの単体テストは一般的に推奨であり、ビジネスロジックが発行するのは完全にOKであるためアプリケーションに対するLINQクエリdataの場合、それは単体テストで完全にOKこれらのLINQクエリである必要があります。
残念ながら、DbFunctionsの機能Entity Frameworkは、LINQクエリを含むテストコードを単体テストする機能を無効にします。さらに、ビジネスロジックレイヤーを特定の永続化テクノロジ(別の説明)に結合するため、ビジネスロジックでDbFunctionsを使用することはアーキテクチャ的に間違っています。
そうは言っても、私たちの目標はLINQクエリを実行する次のようにする能力です:
var orderIdsByDate = (
from o in repo.Orders
group o by o.PlacedAt.Date
// here we used DateTime.Date
// and **NOT** DbFunctions.TruncateTime
into g
orderby g.Key
select new { Date = g.Key, OrderIds = g.Select(x => x.Id) });
ユニットテストでは、これは要約するとLINQ-to-Objects事前に配置されたエンティティのプレーン配列に対して実行されます(たとえば)。実際の実行では、Entity Frameworkのreal ObjectContext
に対して機能する必要があります。
これがそれを達成するためのレシピ-ですが、それはあなたのいくつかのステップを必要とします。私は実際の実用的な例を切り詰めています:
ステップ1。IQueryProvider
の独自のインターセプトラッパーを提供するために、ObjectSet<T>
の独自の実装内にIQueryable<T>
をラップします。
public class EntityRepository<T> : IQueryable<T> where T : class
{
private readonly ObjectSet<T> _objectSet;
private InterceptingQueryProvider _queryProvider = null;
public EntityRepository<T>(ObjectSet<T> objectSet)
{
_objectSet = objectSet;
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _objectSet.AsEnumerable().GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _objectSet.AsEnumerable().GetEnumerator();
}
Type IQueryable.ElementType
{
get { return _objectSet.AsQueryable().ElementType; }
}
System.Linq.Expressions.Expression IQueryable.Expression
{
get { return _objectSet.AsQueryable().Expression; }
}
IQueryProvider IQueryable.Provider
{
get
{
if ( _queryProvider == null )
{
_queryProvider = new InterceptingQueryProvider(_objectSet.AsQueryable().Provider);
}
return _queryProvider;
}
}
// . . . . . you may want to include Insert(), Update(), and Delete() methods
}
ステップ2。インターセプトクエリプロバイダーを実装します。私の例では、EntityRepository<T>
内にネストされたクラスです。
private class InterceptingQueryProvider : IQueryProvider
{
private readonly IQueryProvider _actualQueryProvider;
public InterceptingQueryProvider(IQueryProvider actualQueryProvider)
{
_actualQueryProvider = actualQueryProvider;
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
return _actualQueryProvider.CreateQuery<TElement>(specializedExpression);
}
public IQueryable CreateQuery(Expression expression)
{
var specializedExpression = QueryExpressionSpecializer.Specialize(expression);
return _actualQueryProvider.CreateQuery(specializedExpression);
}
public TResult Execute<TResult>(Expression expression)
{
return _actualQueryProvider.Execute<TResult>(expression);
}
public object Execute(Expression expression)
{
return _actualQueryProvider.Execute(expression);
}
}
ステップ3。最後に、QueryExpressionSpecializer
という名前のヘルパークラスを実装します。これにより、DateTime.Date
がDbFunctions.TruncateTime
に置き換えられます。
public static class QueryExpressionSpecializer
{
private static readonly MethodInfo _s_dbFunctions_TruncateTime_NullableOfDateTime =
GetMethodInfo<Expression<Func<DateTime?, DateTime?>>>(d => DbFunctions.TruncateTime(d));
private static readonly PropertyInfo _s_nullableOfDateTime_Value =
GetPropertyInfo<Expression<Func<DateTime?, DateTime>>>(d => d.Value);
public static Expression Specialize(Expression general)
{
var visitor = new SpecializingVisitor();
return visitor.Visit(general);
}
private static MethodInfo GetMethodInfo<TLambda>(TLambda lambda) where TLambda : LambdaExpression
{
return ((MethodCallExpression)lambda.Body).Method;
}
public static PropertyInfo GetPropertyInfo<TLambda>(TLambda lambda) where TLambda : LambdaExpression
{
return (PropertyInfo)((MemberExpression)lambda.Body).Member;
}
private class SpecializingVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
if ( node.Expression.Type == typeof(DateTime?) && node.Member.Name == "Date" )
{
return Expression.Call(_s_dbFunctions_TruncateTime_NullableOfDateTime, node.Expression);
}
if ( node.Expression.Type == typeof(DateTime) && node.Member.Name == "Date" )
{
return Expression.Property(
Expression.Call(
_s_dbFunctions_TruncateTime_NullableOfDateTime,
Expression.Convert(
node.Expression,
typeof(DateTime?)
)
),
_s_nullableOfDateTime_Value
);
}
return base.VisitMember(node);
}
}
}
もちろん、上記のQueryExpressionSpecializer
の実装を一般化して、任意の数の追加の変換をプラグインできるようにし、カスタムタイプのメンバーをEntityFrameworkに認識されていなくてもLINQクエリで使用できるようにすることができます。
うーん、わかりませんが、このようなことはできませんでしたか?
context.Setup(s => DbFunctions.TruncateTime(It.IsAny<DateTime>()))
.Returns<DateTime?>(new Func<DateTime?,DateTime?>(
(x) => {
/* whatever modification is required here */
return x; //or return modified;
}));