Как написать модульный тест для кода, который использует Entity Framework 4.2? - PullRequest
2 голосов
/ 20 февраля 2012

У меня есть приложение, которое содержит методы, которые работают с данными, используя Entity Framework 4.2 Code First и базу данных MySQL. Я пытаюсь найти хороший способ написать модульный тест MSTest для этих методов. Например:

DataModel:

public class User
{
    public User() { }
    [Key]
    public int UserID { get; set; }
    public string Role { get; set; }
}

public class AppDbContext : DbContext 
{
    public DbSet<User> Users { get; set; }
}

Бизнес-уровень:

public class Bus
{
    public bool UserIsInRole(int userID, string role)
    {
        using(var context = new AppDbContext()) 
        {
            User user = context.Users.SingleOrDefault(p => p.UserID == userID);
            if (user == null)
                return false;
            return user.Roles.Split(',').Contains(role);
        }
    }
}

Я пытаюсь написать набор модульных тестов для функции UserIsInRole, но я хочу попытаться изолировать себя от необходимости читать и записывать в реальную базу данных, поскольку я не могу гарантировать ее состояние перед тестом. Настройка / удаление базы данных только для этого теста займет слишком много времени.

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

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

Как бы вы изменили вышеприведенное и написали набор тестов, проверяющих, что метод UserIsInRole:

  1. возвращает false, если userID не существует в Users коллекция.
  2. возвращает false, если пользователь не содержит желаемая роль.
  3. возвращает true, если у пользователя есть требуемая роль.

Имейте в виду, что это упрощенный пример и что код может фактически содержать несколько запросов произвольной сложности, поэтому я надеюсь найти что-то более всеобъемлющее, чем, скажем, перемещение каждого запроса в виртуальную функцию, которая заменяется на тестовый фреймворк для возврата предопределенной пользовательской записи.

Ответы [ 5 ]

3 голосов
/ 20 февраля 2012

Ваш код не подлежит тестированию.Как вы хотите подделать что-то, если вы используете new непосредственно в тестируемой системе?

Улучшите ваш код:

public class Bus
{
    public bool UserIsInRole(int userID, string role)
    {
        using(var context = CreateContext()) 
        {
            User user = ExecuteGetUserQuery(context, userId);
            if (user == null)
                return false;
            return user.Roles.Split(',').Contains(role);
        }
    }

    protected virtual IAppDbContext CreateContext() 
    {
        return new AppDbContext();
    }

    protected virtual User ExecuteGetUserQuery(IAppDbContext context, int userId)
    {
        return context.Users.SingleOrDefault(p => p.UserID == userID);
    }
}

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

  • Первое, что мы сделали, это применили принцип единой ответственности и разделили ваш метод на три отдельные обязанности:
    • Создание контекста
    • Запросвыполнение
    • UserIsInRole логика
  • Мы добавили интерфейс для контекста, чтобы сделать вашу логику зависимой от абстракции, а не от реализации
  • Мы также добавили несколько хуков для заменыреализация в тестах

Если вы хотите написать модульный тест для UserIsInRole (и других чистых модульных тестов), вы можете создать класс реализации Bus и получить любые поддельные данные из переопределенной версии ExecuteGetUserQuery.Переопределив CreateContext, вы также сделаете свои тесты полностью независимыми от базы данных или EF.Переопределение этих методов не приведет к тестированию другой логики, потому что вы все равно будете подделывать эти данные.Протестированный метод UserIsInRole не изменяется в производном классе.

Конечно, вместо предоставления виртуальных методов вы можете переместить эту функциональность в отдельный класс или классы и использовать заглушки или макеты, но для простых сценариев это работает.Если вам нужно проверить взаимодействие с базой данных, вы напишите интеграционный тест только для ExecuteGetUserQuery.

2 голосов
/ 20 февраля 2012

Я бы отделил знание EF от остальной части вашего домена.Если вы отключите DbSet, вы обнаружите, что он реализует IQueryable, которого достаточно для работы EF.Создайте интерфейс, который определяет контекст вашего домена, и сделайте так, чтобы ваши различные конкретные реализации (EF и Fake) реализовали этот интерфейс, например:

public class User 
{ 
    public User() { } 
    [Key] 
    public int UserID { get; set; } 
    public string Role { get; set; } 
} 

public interface IAppDomain
{
    public IQueryable<User> Users { get; }
}

public class AppDbContext : DbContext, IAppDomain
{ 
    // exposure for EF
    public DbSet<User> Users { get; set; } 

    IAppDomain.IQueryable<User> Users { get { return ((AppDbContext)this).Users; }
} 

public class FakeAppDomain : IAppDomain
{
    private List<User> _sampleUsers = new List<User>(){
        new User() { UserID = 1, Role = "test" }
    }

    public IQueryable<User> Users { get { return _sampleUsers; } }
}

. Это можно использовать следующими способами:

IQueryable<User> GetUsersByManagerRole(IAppDomain domain)
{
    return from u in domain.Users
           where u.Role == "Manager"
           select u;
}

Это позволяет вам создать поддельную реализацию, которая принимает любой тип вводимых данных.Далее в модульном тесте вы создаете новый FakeDomainContext, в котором вы устанавливаете состояние желаемым образом для вашего модульного теста.Хотите проверить пользователей с определенной ролью можно найти?Создайте FakeDomainContext с пользователями с некоторыми тестовыми ролями и попытайтесь их найти.Легко и чисто.

0 голосов
/ 20 февраля 2012

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

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

Вотссылка на мой блог об этом и как я лично решил проблему.То, что я сделал, хорошо мне помогло, поэтому посмотрите на пример и на то, как я реализовал модульные тесты с внутренним хранилищем данных в EF 4.

http://blog.staticvoid.co.nz/2011/10/staticvoid-repository-pattern-nuget.html

0 голосов
/ 20 февраля 2012

Если вам не нравится издеваться над DbContext, вы можете смоделировать объекты данных и извлечь часть логики бизнеса в независимые от БД тестируемые методы.

public class Bus
{
    private UnitTestable impl;

    public bool UserIsInRole(int userID, string role)
    {
        using(var context = new AppDbContext()) 
        {
            return impl.UserInRole(context.Users, role);
        }
    }
}

public class UnitTestable
{
    public bool UserInRole(IQueryable<User> users, string role)
    {
        User user = users.SingleOrDefault(p => p.UserID == userID);
        if (user == null)
            return false;
        return user.Roles.Split(',').Contains(role);
    }
}

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

0 голосов
/ 20 февраля 2012

Взгляните на пост Роуэна о модульном тестировании с поддельным контекстом БД: http://romiller.com/2012/02/14/testing-with-a-fake-dbcontext/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...