web-dev-qa-db-ja.com

MoqでEF DbContextをモックする

模擬DbContextを使用して、サービスの単体テストを作成しようとしています。次の機能を備えたインターフェースIDbContextを作成しました。

public interface IDbContext : IDisposable
{
    IDbSet<T> Set<T>() where T : class;
    DbEntityEntry<T> Entry<T>(T entity) where T : class;
    int SaveChanges();
}

私の実際のコンテキストは、このインターフェースIDbContextおよびDbContextを実装しています。

今、コンテキストでIDbSet<T>をモックしようとしているので、代わりにList<User>を返します。

[TestMethod]
public void TestGetAllUsers()
{
    // Arrange
    var mock = new Mock<IDbContext>();
    mock.Setup(x => x.Set<User>())
        .Returns(new List<User>
        {
            new User { ID = 1 }
        });

    UserService userService = new UserService(mock.Object);

    // Act
    var allUsers = userService.GetAllUsers();

    // Assert
    Assert.AreEqual(1, allUsers.Count());
}

私は常に.Returnsでこのエラーを受け取ります:

The best overloaded method match for
'Moq.Language.IReturns<AuthAPI.Repositories.IDbContext,System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>.Returns(System.Func<System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>)'
has some invalid arguments
58
Gaui

FakeDbSet<T>を実装するIDbSet<T>クラスを作成することでそれを解決できました

public class FakeDbSet<T> : IDbSet<T> where T : class
{
    ObservableCollection<T> _data;
    IQueryable _query;

    public FakeDbSet()
    {
        _data = new ObservableCollection<T>();
        _query = _data.AsQueryable();
    }

    public virtual T Find(params object[] keyValues)
    {
        throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
    }

    public T Add(T item)
    {
        _data.Add(item);
        return item;
    }

    public T Remove(T item)
    {
        _data.Remove(item);
        return item;
    }

    public T Attach(T item)
    {
        _data.Add(item);
        return item;
    }

    public T Detach(T item)
    {
        _data.Remove(item);
        return item;
    }

    public T Create()
    {
        return Activator.CreateInstance<T>();
    }

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
    {
        return Activator.CreateInstance<TDerivedEntity>();
    }

    public ObservableCollection<T> Local
    {
        get { return _data; }
    }

    Type IQueryable.ElementType
    {
        get { return _query.ElementType; }
    }

    System.Linq.Expressions.Expression IQueryable.Expression
    {
        get { return _query.Expression; }
    }

    IQueryProvider IQueryable.Provider
    {
        get { return _query.Provider; }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        return _data.GetEnumerator();
    }
}

これで、私のテストは次のようになります。

[TestMethod]
public void TestGetAllUsers()
{
    //Arrange
    var mock = new Mock<IDbContext>();
    mock.Setup(x => x.Set<User>())
        .Returns(new FakeDbSet<User>
        {
            new User { ID = 1 }
        });

    UserService userService = new UserService(mock.Object);

    // Act
    var allUsers = userService.GetAllUsers();

    // Assert
    Assert.AreEqual(1, allUsers.Count());
}
29
Gaui

素晴らしいアイデアをありがとう、Gaui =)

ソリューションにいくつかの改善を加えたので、共有したいと思います。

  1. FakeDbSetは、AddRange()などの追加メソッドを取得するためにDbSetからも継承されます
  2. ObservableCollection<T>List<T>に置き換えて、List<>に既に実装されているすべてのメソッドをFakeDbSetまで渡しました

私のFakeDbSet:

    public class FakeDbSet<T> : DbSet<T>, IDbSet<T> where T : class {
    List<T> _data;

    public FakeDbSet() {
        _data = new List<T>();
    }

    public override T Find(params object[] keyValues) {
        throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
    }

    public override T Add(T item) {
        _data.Add(item);
        return item;
    }

    public override T Remove(T item) {
        _data.Remove(item);
        return item;
    }

    public override T Attach(T item) {
        return null;
    }

    public T Detach(T item) {
        _data.Remove(item);
        return item;
    }

    public override T Create() {
        return Activator.CreateInstance<T>();
    }

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T {
        return Activator.CreateInstance<TDerivedEntity>();
    }

    public List<T> Local {
        get { return _data; }
    }

    public override IEnumerable<T> AddRange(IEnumerable<T> entities) {
        _data.AddRange(entities);
        return _data;
    }

    public override IEnumerable<T> RemoveRange(IEnumerable<T> entities) {
        for (int i = entities.Count() - 1; i >= 0; i--) {
            T entity = entities.ElementAt(i);
            if (_data.Contains(entity)) {
                Remove(entity);
            }
        }

        return this;
    }

    Type IQueryable.ElementType {
        get { return _data.AsQueryable().ElementType; }
    }

    Expression IQueryable.Expression {
        get { return _data.AsQueryable().Expression; }
    }

    IQueryProvider IQueryable.Provider {
        get { return _data.AsQueryable().Provider; }
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return _data.GetEnumerator();
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator() {
        return _data.GetEnumerator();
    }
}

DbSetの変更とEFコンテキストオブジェクトのモックは非常に簡単です。

    var userDbSet = new FakeDbSet<User>();
    userDbSet.Add(new User());
    userDbSet.Add(new User());

    var contextMock = new Mock<MySuperCoolDbContext>();
    contextMock.Setup(dbContext => dbContext.Users).Returns(userDbSet);

Linqクエリを実行できるようになりましたが、外部キー参照が自動的に作成されない可能性があることに注意してください。

    var user = contextMock.Object.Users.SingeOrDefault(userItem => userItem.Id == 42);

コンテキストオブジェクトがモックされているため、Context.SaveChanges()は何も実行せず、エンティティのプロパティ変更がdbSetに入力されない場合があります。 SetModifed()メソッドをモックして変更を取り込むことでこれを解決しました。

16
szuuuken

誰かがまだ興味がある場合、私は同じ問題を抱えていて、この記事は非常に役に立ちました: Mocking Frameworkを使用したEntity Framework Testing(EF6以降)

これはEntity Framework 6以降にのみ適用されますが、単純なSaveChangesテストから、すべてMoq(およびいくつかの手動クラス)を使用した非同期クエリテストまですべてをカバーします。

12
eitamal

誰かがまだ答えを探しているなら、私は 小さなライブラリ を実装してDbContextのモックを許可しました。

ステップ1

インストール Coderful.EntityFramework.Testing nugetパッケージ:

Install-Package Coderful.EntityFramework.Testing

ステップ2

次に、次のようなクラスを作成します。

internal static class MyMoqUtilities
{
    public static MockedDbContext<MyDbContext> MockDbContext(
        IList<Contract> contracts = null,
        IList<User> users = null)
    {
        var mockContext = new Mock<MyDbContext>();

        // Create the DbSet objects.
        var dbSets = new object[]
        {
            MoqUtilities.MockDbSet(contracts, (objects, contract) => contract.ContractId == (int)objects[0] && contract.AmendmentId == (int)objects[1]),
            MoqUtilities.MockDbSet(users, (objects, user) => user.Id == (int)objects[0])
        };

        return new MockedDbContext<SourcingDbContext>(mockContext, dbSets); 
    }
}

ステップ3

モックを非常に簡単に作成できるようになりました。

// Create test data.
var contracts = new List<Contract>
{
    new Contract("#1"),
    new Contract("#2")
};

var users = new List<User>
{
    new User("John"),
    new User("Jane")
};

// Create DbContext with the predefined test data.
var dbContext = MyMoqUtilities.MockDbContext(
    contracts: contracts,
    users: users).DbContext.Object;

そして、モックを使用します。

// Create.
var newUser = dbContext.Users.Create();

// Add.
dbContext.Users.Add(newUser);

// Remove.
dbContext.Users.Remove(someUser);

// Query.
var john = dbContext.Users.Where(u => u.Name == "John");

// Save changes won't actually do anything, since all the data is kept in memory.
// This should be ideal for unit-testing purposes.
dbContext.SaveChanges();

完全な記事: http://www.22bugs.co/post/Mocking-DbContext/

5
niaher

このMSDN の記事に基づいて、DbContextおよびDbSetをモックするための独自のライブラリを作成しました。

どちらもNuGetとGitHubで利用できます。

これらのライブラリを作成した理由は、SaveChanges動作をエミュレートし、同じ主キーを持つモデルを挿入するときにDbUpdateExceptionをスローし、複数列/自動インクリメントの主キーをサポートするためです。モデル。

さらに、DbSetMockDbContextMockの両方がMock<DbSet>Mock<DbContextを継承するため、 Moqフレームワーク のすべての機能を使用できます。

Moqの隣には、NSubstituteの実装もあります。

Moqバージョンでの使用法は次のようになります。

public class User
{
    [Key, Column(Order = 0)]
    public Guid Id { get; set; }

    public string FullName { get; set; }
}

public class TestDbContext : DbContext
{
    public TestDbContext(string connectionString)
        : base(connectionString)
    {
    }

    public virtual DbSet<User> Users { get; set; }
}

[TestFixture]
public class MyTests
{
    var initialEntities = new[]
        {
            new User { Id = Guid.NewGuid(), FullName = "Eric Cartoon" },
            new User { Id = Guid.NewGuid(), FullName = "Billy Jewel" },
        };

    var dbContextMock = new DbContextMock<TestDbContext>("fake connectionstring");
    var usersDbSetMock = dbContextMock.CreateDbSetMock(x => x.Users, initialEntities);

    // Pass dbContextMock.Object to the class/method you want to test

    // Query dbContextMock.Object.Users to see if certain users were added or removed
    // or use Mock Verify functionality to verify if certain methods were called: usersDbSetMock.Verify(x => x.Add(...), Times.Once);
}
3
huysentruitw

遅くなりましたが、この記事は役に立ちました: InMemoryによるテスト (MSDN Docs)。

わずかなコーディングとDBContext実装を実際にテストする機会を活用して、メモリ内のDBコンテキスト(データベースではない)を使用する方法について説明します。

2