Модульный тест EF Шаблон репозитория с Moq - PullRequest
6 голосов
/ 02 сентября 2011

Я решил начать писать модульные тесты в нашем приложении.Он использует Entity Framework с шаблоном хранилища.

Теперь я хочу начать тестирование логических классов, которые используют хранилища.Здесь я приведу простой пример.

Три моих метода в классе GenericRepository:

public class GenericRepository : IRepository
{
    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        var entityName = GetEntityName<TEntity>();
        return Context.CreateQuery<TEntity>(entityName);
    }
    private string GetEntityName<TEntity>() where TEntity : class
    {
        return typeof(TEntity).Name;
    }
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

Простой логический класс, возвращающий отдельные годы из календарной таблицы в порядке убывания (да, я знаю,в нашем коде слово слово написано с ошибкой):

public class GetDistinctYearsFromCalendar
{
    private readonly IRepository _repository;

    public GetDistinctYearsFromCalendar()
    {
        _repository = new GenericRepository();
    }

    internal GetDistinctYearsFromCalendar(IRepository repository)
    {
        _repository = repository;
    }

    public int[] Get()
    {
        return _repository.Find<Calender_Tbl>(c => c.Year.HasValue).Select(c => c.Year.Value).Distinct().OrderBy(c => c).Reverse().ToArray();
    }
}

И вот мой первый тест:

[TestFixture]
public class GetDistinctYearsFromCalendarTest
{
    [Test]
    public void ReturnsDistinctDatesInCorrectOrder()
    {
        var repositoryMock = new Mock<IRepository>();

        repositoryMock.Setup(r => r.Find<Calender_Tbl>(c => c.Year.HasValue)).Returns(new List<Calender_Tbl>
        {
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2010, 1, 1),
                  Year = 2010
              },
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2010, 2, 1),
                  Year = 2010
              },
           new Calender_Tbl
              {
                  Date =
                      new DateTime(2011, 1, 1),
                  Year = 2011
              }
        }.AsQueryable());

        var getDistinct = new GetDistinctYearsFromCalendar(repositoryMock.Object).Get();

        Assert.AreEqual(2, getDistinct.Count(), "Returns more years than distinct.");
        Assert.AreEqual(2011, getDistinct[0], "Incorrect order, latest years not first.");
        Assert.AreEqual(2010, getDistinct[1], "Wrong year.");


    }
}

Это работает нормально.Но на самом деле это не то, что я хочу сделать.Поскольку мне нужно настроить метод Find для фиктивного объекта, мне также нужно знать, как он будет вызываться в моем логическом классе.Если я хотел бы сделать TDD, я не хочу возражать по этому поводу.Все, что я хочу знать, это то, какие объекты Календаря должен предоставлять мой репозиторий.Я хотел бы настроить метод GetQuery.Например:

repositoryMock.Setup(r => r.GetQuery<Calender_Tbl>()).Returns(new List<Calender_Tbl>
{
  new Calender_Tbl
      {
          Date =
              new DateTime(2010, 1, 1),
          Year = 2010
      },
  new Calender_Tbl
      {
          Date =
              new DateTime(2010, 2, 1),
          Year = 2010
      },
  new Calender_Tbl
      {
          Date =
              new DateTime(2011, 1, 1),
          Year = 2011
      }
}.AsQueryable());

Поэтому, когда Find вызывает внутренний метод GetQuery в классе GenericRepository, он должен получить правильные объекты Calendar, которые я настроил в GetQuery.Но это не работает, конечно.Поскольку я не настроил метод Find моего фиктивного объекта, я не получаю никаких сущностей.

Так что же делать?Конечно, я мог бы использовать Moles или какой-то другой фреймворк, который все издевается, но я не хочу этого делать.Могу ли я что-нибудь сделать в дизайне класса или теста, чтобы решить проблему?

Это не конец света, если я должен идти с моим текущим решением, но что, если год собственности превращается вне обнуляемый int?Тогда, конечно, мне придется изменить свою реализацию в классе логики, но мне также придется изменить тест.Я хотел бы попытаться избежать этого.

Ответы [ 2 ]

12 голосов
/ 02 сентября 2011

Я вижу два пути:

public class MockRepository : IRepository
{
    private List<object> entities;
    public MockRepository(params object[] entitites)
    {
      this.entities = entities.ToList();
    }

    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        return this.entities.OfType<TEntity>().AsQueryable();
    }

    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

Это самый простой и мой любимый способ.Moq не молоток для всего;)

В качестве альтернативы, если вы действительно настаиваете на использовании Moq (я польщен, но в этом случае это очень не нужно, так как вы можете проводить основанное на состоянии тестирование возвращаемого значения).лица), вы можете сделать:

public class GenericRepository : IRepository
{
    public virtual IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {
        var entityName = GetEntityName<TEntity>();
        return Context.CreateQuery<TEntity>(entityName);
    }
    private string GetEntityName<TEntity>() where TEntity : class
    {
        return typeof(TEntity).Name;
    }
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().Where(predicate).AsEnumerable();
    }
}

и затем использовать Moq для переопределения поведения GetQuery:

var repository = new Mock<GenericRepository> { CallBase = true };

repository.Setup(x => x.GetQuery<Foo>()).Returns(theFoos.AsQueryable());

Что произойдет, это то, что метод Find будет выполняться в GenericRepositoryкласс, который, в свою очередь, будет использовать GetQuery, который был перезаписан Moq для предоставления фиксированного набора сущностей.

Я явно устанавливаю CallBase = true на тот случай, если вам удастся сделать Find виртуальным, так что мы гарантируемэто всегда называется.Технически не требуется, если Find не является виртуальным, так как он всегда будет вызываться для фактического класса, от которого mock наследует / mocking.

Я бы выбрал первый вариант, гораздо проще понять, что происходити его можно повторно использовать вне контекста отдельного конкретного теста (просто пропустите все необходимые объекты, и он будет работать для всего).

0 голосов
/ 09 сентября 2015

Недавно появился новый инструмент под названием Effort для EF 6+, который я считаю чрезвычайно полезным для модульного тестирования на поддельных БД. Смотри http://effort.codeplex.com/wikipage?title=Tutorials&referringTitle=Home.

Добавьте его с помощью команды консоли диспетчера пакетов:

PM> Install-Package Effort.EF6

Затем добавьте интерфейс для вашего DbContext, скажем, если вы используете базу данных AdventureWorks (см. https://sql2012kitdb.codeplex.com/):

Затем обновите ваш DbContext, чтобы добавить два новых параметризованных конструктора:

    /// 
    /// Create a new context based on database name or connection string.
    /// 
    /// Database name or connection string
    public AdventureWorksEntities(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

    public AdventureWorksEntities(DbConnection connection)
        : base(connection, true)
    {
        this.Configuration.LazyLoadingEnabled = false;
    }

Добавьте конструктор, который принимает интерфейс к вашему хранилищу:

    private IAdventureWorksDbContext _dbContext;

    public ProductRepository(IAdventureWorksDbContext dbContext)
    {
        dbContext.Configuration.AutoDetectChangesEnabled = false;
        this._dbContext = dbContext;
    }

Затем добавьте интерфейс к вашему проекту модульного тестирования и связанному с ним классу:

public interface ITestDatabase : IDisposable
{
    IAdventureWorksDbContext CreateContext();

    void Dispose(IAdventureWorksDbContext context);
}

Добавьте некоторые поддельные данные в ваш проект модульного тестирования:

public class ProductsTestData
{
    public static void AddTestData(IAdventureWorksDbContext dbContext)
    {
        dbContext.Products.Add(new Product() { Id = new Guid("23ab9e4e-138a-4223-bb42-1dd176d8583cB"), Name = "Product A", CreatedDate = DateTime.Now, Description = "Product description..." });
        dbContext.Products.Add(new Product() { Id = new Guid("97e1835f-4c1b-4b87-a514-4a17c019df00"), Name = "Product B", CreatedDate = DateTime.Now });
        dbContext.SaveChanges();
    }
}

Теперь настройте класс юнит-тестирования:

[TestClass]
public class ProductsTest
{
    private ITestDatabase _testDatabaseStrategy;
    private ProductRepository _productRepository;
    private IAdventureWorksDbContext _context;

    [TestInitialize]
    public void SetupTest()
    {
        // create the test strategy.  This will initialise a new database
        _testDatabaseStrategy = CreateTestStrategy();

        // add test data to the database instance
        using (_context = _testDatabaseStrategy.CreateContext())
        {
            ProductsTestData.AddTestData(_context);
            _context.SaveChanges();
        }

        // initialise the repository we are testing
        _context = _testDatabaseStrategy.CreateContext();
        _productRepository = new ProductRepository(_context);
    }

    protected ITestDatabase CreateTestStrategy()
    {
        return new EffortDatabaseStrategy();
    }

    [TestCleanup]
    public void CleanupTest()
    {
        // dispose of the database and connection
        _testDatabaseStrategy.Dispose(_context);
        _context = null;
    }

    [TestMethod]
    public void GetProductsByTagName()
    {
        IEnumerable<Product> products = _productRepository.GetProductsByTagName("Tag 1", false);
        Assert.AreEqual(1, products.Count());
    }

Где EffortDatabaseStrategy:

public class EffortDatabaseStrategy : ITestDatabase
{
    public EffortDatabaseStrategy()
    {
    }

    private DbConnection _connection;

    public IAdventureWorksDbContext CreateContext()
    {
        if (_connection == null)
        {
            _connection = Effort.DbConnectionFactory.CreateTransient();
        }
        var context = new AdventureWorksDbContext(_connection);

        return context;
    }

    public void Dispose(IAdventureWorksDbContext context)
    {
        if (context != null)
        {
            context.Dispose();
        }
    }

    public void Dispose()
    {
    }
}

Для получения полной информации, пожалуйста, смотрите http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.

...