この質問の背景を説明したいと思います。必要に応じてスキップします。かなり長い間、EFに関連するコードのテストに関して、stackoverflowなどの進行中の議論に細心の注意を払ってきました。ある陣営は、Linq to Objects&Sqlと実装の違いのために、データベースに対して直接テストします。別の人は、モックによるテストを言います。
別の意見の分裂は、リポジトリの使用、またはDbContextとDbSetがすでに作業単位とリポジトリパターンを提供していることを受け入れることの問題です。私がEFを使用していた頃、私はこれらのキャンプから提供された意見のあらゆる組み合わせについて試してきました。私がやったことに関係なく、EFはテストするのが難しいことがわかります。
EFチームがEF 6で DbSetをよりモック化可能 にしたことを知り、興奮しました。また、Moqを使用した非同期メソッドを含むDbSetをモック化する方法について documentation も提供しました。 Web Apiを含む最新のプロジェクトに取り組んでいると、EFをモックできれば、リポジトリの作成をスキップできることに気付きました。 this ... などのブログ記事をいくつか読んだ後にインスピレーションを得ました
-背景の終わり---
実際の問題は、EFチームがDbSetをMoqする方法に関するコード例を示した後、いずれかのコードで.Include()を使用すると、ArgumentNullExceptionがスローされることです。
その他 SOに関する関連記事
DbContextのインターフェイスは次のとおりです。
public interface ITubingForcesDbContext
{
DbSet<WellEntity> Wells { get; set; }
int SaveChanges();
Task<int> SaveChangesAsync();
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
これは私のコントローラーが扱う主要なエンティティです
public class WellEntity
{
public int Id { get; set; }
public DateTime DateUpdated { get; set; }
public String UpdatedBy { get; set; }
[Required]
public string Name { get; set; }
public string Location { get; set; }
public virtual Company Company { get; set; }
public virtual ICollection<GeometryItem> GeometryItems
{
get { return _geometryItems ?? (_geometryItems = new Collection<GeometryItem>()); }
protected set { _geometryItems = value; }
}
private ICollection<GeometryItem> _geometryItems;
public virtual ICollection<SurveyPoint> SurveyPoints
{
get { return _surveyPoints ?? (_surveyPoints = new Collection<SurveyPoint>()); }
protected set { _surveyPoints = value; }
}
private ICollection<SurveyPoint> _surveyPoints;
public virtual ICollection<TemperaturePoint> TemperaturePoints
{
get { return _temperaturePoints ?? (_temperaturePoints = new Collection<TemperaturePoint>()); }
protected set { _temperaturePoints = value; }
}
private ICollection<TemperaturePoint> _temperaturePoints;
}
EF DbContextを直接使用するコントローラーは次のとおりです。
[Route("{id}")]
public async Task<IHttpActionResult> Get(int id)
{
var query = await TheContext.Wells.
Include(x => x.GeometryItems).
Include(x => x.SurveyPoints).
Include(x => x.TemperaturePoints).
SingleOrDefaultAsync(x => x.Id == id);
if (query == null)
{
return NotFound();
}
var model = ModelFactory.Create(query);
return Ok(model);
}
最後に、失敗したテストを示します...
テスト設定 - -
[ClassInitialize]
public static void ClassInitialize(TestContext testContest)
{
var well1 = new WellEntity { Name = "Well 1" };
var well2 = new WellEntity { Name = "Well 2" };
var well3 = new WellEntity { Name = "Well 3" };
var well4 = new WellEntity { Name = "Well 4" };
well1.GeometryItems.Add(new GeometryItem());
well1.TemperaturePoints.Add(new TemperaturePoint());
well1.SurveyPoints.Add(new SurveyPoint());
well2.GeometryItems.Add(new GeometryItem());
well2.TemperaturePoints.Add(new TemperaturePoint());
well2.SurveyPoints.Add(new SurveyPoint());
well3.GeometryItems.Add(new GeometryItem());
well3.TemperaturePoints.Add(new TemperaturePoint());
well3.SurveyPoints.Add(new SurveyPoint());
well4.GeometryItems.Add(new GeometryItem());
well4.TemperaturePoints.Add(new TemperaturePoint());
well4.SurveyPoints.Add(new SurveyPoint());
var wells = new List<WellEntity> { well1, well2, well3, well4 }.AsQueryable();
var mockWells = CreateMockSet(wells);
_mockContext = new Mock<ITubingForcesDbContext>();
_mockContext.Setup(c => c.Wells).Returns(mockWells.Object);
}
private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data) where T : class
{
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IDbAsyncEnumerable<T>>()
.Setup(m => m.GetAsyncEnumerator())
.Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
mockSet.As<IQueryable<T>>()
.Setup(m => m.Provider)
.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<T>>().Setup(m =>m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<T>>().Setup(m=>m.GetEnumerator()).
Returns(data.GetEnumerator());
return mockSet;
}
[TestMethod]
public async Task Get_ById_ReturnsWellWithAllChildData()
{
// Arrange
var controller = new WellsController(_mockContext.Object);
// Act
var actionResult = await controller.Get(1);
// Assert
var response = actionResult as OkNegotiatedContentResult<WellModel>;
Assert.IsNotNull(response);
Assert.IsNotNull(response.Content.GeometryItems);
Assert.IsNotNull(response.Content.SurveyPoints);
Assert.IsNotNull(response.Content.TemperaturePoints);
}
TestDbAsyncQueryProviderおよびTestDbAsyncEnumeratorは、参照されているEFチームのドキュメントから直接取得されます。私は、モック用のデータを作成する方法についていくつかの異なるバリエーションを試しましたが、運がありませんでした。
NSubstituteおよびEntity Framework 6 +の.Include("Foo")
問題を解決する方法に興味を持ってこの問題に出くわした人のために、私はInclude
呼び出しをバイパスすることができました次の方法で:
var data = new List<Foo>()
{
/* Stub data */
}.AsQueryable();
var mockSet = Substitute.For<DbSet<Foo>, IQueryable<Foo>>();
((IQueryable<Post>)mockSet).Provider.Returns(data.Provider);
((IQueryable<Post>)mockSet).Expression.Returns(data.Expression);
((IQueryable<Post>)mockSet).ElementType.Returns(data.ElementType);
((IQueryable<Post>)mockSet).GetEnumerator().Returns(data.GetEnumerator());
// The following line bypasses the Include call.
mockSet.Include(Arg.Any<string>()).Returns(mockSet);
Moqを使用した完全な例を次に示します。サンプル全体を単体テストクラスに貼り付けることができます。 @ jbaum012と@Skuliのコメントに感謝します。 Microsoftの優れたチュートリアル もお勧めします。
// An Address entity
public class Address
{
public int Id { get; set; }
public string Line1 { get; set; }
}
// A Person referencing Address
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public virtual Address Address { get; set; }
}
// A DbContext with persons and devices
// Note use of virtual (see the tutorial reference)
public class PersonContext : DbContext
{
public virtual DbSet<Person> Persons { get; set; }
public virtual DbSet<Address> Addresses { get; set; }
}
// A simple class to test
// The dbcontext is injected into the controller
public class PersonsController
{
private readonly PersonContext _personContext;
public PersonsController(PersonContext personContext)
{
_personContext = personContext;
}
public IEnumerable<Person> GetPersons()
{
return _personContext.Persons.Include("Address").ToList();
}
}
// Test the controller above
[TestMethod]
public void GetPersonsTest()
{
var address = new Address { Id = 1, Line1 = "123 Main St." };
var expectedPersons = new List<Person>
{
new Person { Id = 1, Address = address, Name = "John" },
new Person { Id = 2, Address = address, Name = "John Jr." },
};
var mockPersonSet = GetMockDbSet(expectedPersons.AsQueryable());
mockPersonSet.Setup(m => m.Include("Address")).Returns(mockPersonSet.Object);
var mockPersonContext = new Mock<PersonContext>();
mockPersonContext.Setup(o => o.Persons).Returns(mockPersonSet.Object);
// test the controller GetPersons() method, which leverages Include()
var controller = new PersonsController(mockPersonContext.Object);
var actualPersons = controller.GetPersons();
CollectionAssert.AreEqual(expectedPersons, actualPersons.ToList());
}
// a helper to make dbset queryable
private Mock<DbSet<T>> GetMockDbSet<T>(IQueryable<T> entities) where T : class
{
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(entities.Provider);
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(entities.Expression);
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(entities.ElementType);
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(entities.GetEnumerator());
return mockSet;
}
これで遊んで、ここで答えを参照します 拡張メソッドの呼び出しのセットアップ結果 Moqは静的拡張メソッドをモックできないようです
私は追加しようとしました:
mockSet.Setup(t => t.FirstAsync()).Returns(Task.FromResult(data.First()));
mockSet.Setup(t => t.FirstAsync(It.IsAny<Expression<Func<T, bool>>>())).Returns(Task.FromResult(data.First()));
Moqは次のように文句を言います:
System.NotSupportedException:式は、モックされたオブジェクトに属さないメソッドを参照します:t => t.FirstAsync()
したがって、3つのオプションがあるようです。
私は一般的なアプローチでMoqにIncludeをモックすることができました。これは、文字列と式のみを含むInclude()のすべての使用法をカバーしていませんが、私のニーズに合っています:
public Mock<DbSet<T>> SetupMockSetFor<T>(Expression<Func<DbContext, DbSet<T>>> selector) where T : class
{
var mock = new Mock<DbSet<T>>();
mock.ResetCalls();
this.EntitiesMock.Setup(m => m.Set<T>()).Returns(mock.Object);
this.EntitiesMock.Setup(selector).Returns(mock.Object);
mock.Setup(x => x.Include(It.IsAny<string>())).Returns(mock.Object);
try
{
mock.Setup(x => x.Include(It.IsAny<Expression<Func<T, object>>>()))
.Returns(mock.Object);
}
catch
{
// Include only applies to some objects, ignore where it doesn't work
}
return mock;
}
テスト使用法:
var mockCourseSet = SetupMockSetFor(entities => entities.Courses);
サービス方式:
var foundCourses = dbContext.Courses.Include(c => c.CourseParticipants).Where(c => c.Id = courseId)
EFチームが提供するDbSet
の例は、単なる例です。
Include
(またはFindAsync
) をモックしたい場合は、自分でやらなければなりません。