четверг, 21 февраля 2013 г.

Fakes Framework при тестировании методов использующих Entity Framework

Название получилось длинное, но как сказать короче, про что сегодня пойдет речь, я не придумал. Итак, под катом, будет:
1. Как создать Fakes Assembly для System.Core.dll (как не странно это звучит, но методы First, Where и другие, да и все пространство System.Linq находиться именно в этой сборке).
2. Как сделать Fake методы для присоединенных методов типа Single и Include.
3. Как вынести инициализацию FakeContext-а и переопределение методов которые не меняются от тестового метода к тестовому методу в отдельный метод, который будет вызываться автоматически.

Начну с небольшой предыстории. У нас месяцев N-цать назад, был написан большой проект. Но то одно, то второе. В общем запускается он со следующего месяца. И тут, в преддверии запуска, у заказчика появляются новые пожелания. Код писался давно, в основном визуалка и несколько методов бизнес-логики вынесенные на сервер. Пожелания затрагивают бизнес-логику, поэтому принято решение, покрыть серверный код модульными тестами.
Немного о структуре проекта:
Клиентская часть написана на SilverLight, сервер состоит из уровня доступа к данным (Data), уровня бизнес-логики (BRL) и Web-проекта, который отвечает за публикацию сервисов. Вот методы BRL проекта и предстоит покрывать тестами.
На этом предыстория заканчивается и начинается история.
Вот фрагмент метода, который предстоит тестировать:

public string RunAccessRequestWorkflow(Guid p_AccessRequestId)
{
    string emailMessage = string.Empty;
    bool result = true;
    string stepMsg = string.Empty;
    //выбрать AccessRequest по p_AccessRequestId
    AccessRequest currentAccessRequest = this.DbContext.AccessRequests
        .Include(ar => ar.Employee.Person)
        .Include(ar => ar.Initiator.Person)
        .Include(ar => ar.AlternateEmployee.Person)
        .Include(ar => ar.RolesByAccessRequests.Select(rbar => rbar.Role.ApprovalProcedure.Stages.Select(s => s.Template)))
        .Include(ar => ar.RolesByAccessRequests.Select(rbar => rbar.Role.SoftwareItem))
        .SingleOrDefault(ar => ar.Id == p_AccessRequestId);
        if (currentAccessRequest != null)
        {
            //обработка запроса
        }
        else
        {
            //запрошенная заявка не найдена
            errorMessage = "Заявка с указанным идентификатором не найдена";
        }
    return errorMessage;
}
Все, создаем тестовый проект и пытаемся среди его References найти System.Core, чтобы создать для него Fakes Assembly:
А его то и нет. Ну ничего, выбираем Add Reference, ищем System.Core в списке и при попытке его добавить, получаем вот такой отлуп:
Чтобы решить проблему, я создал проект по шаблону Class Library, в нем System.Core виден. В нем создал Fakes Assambly, а потом скопировал все что создалось в тестовый проект. Уже после того, как у меня все заработало, мне на форумах MSDN (здесь), дали ссылку на вот это решение. Т.к. по ссылке на английском, то на всякий пожарный, перевожу здесь.
Необходимо файл проекта с тестами открыть в текстовом редакторе. Найти элемент отвечающий за добавление ссылки на System и добавить аналогичный для ссылки на System.Core:
После сохранения изменений и перехода в Visual Studio, будет предложено перечитать проект. Ура! System.Core появилось в списке сборок. Правый клик по нему и, выбрав Add Fakes Assembly, получим:
Отлично, можем переходить к написанию тестовых методов.
Первый метод, который напрашивается на переопределение, это конечно метод SingleOrDefault. Ну а для того, чтобы его переопределить, нужно его найти в Fake-овом классе, который создан для Querable. В тестовом методе пишем строку: System.Linq.Fakes.ShimQueryable, а затем открыв контекстное меню на  ShimQueryable переходим по "Go To Difinition". В открывшемся файле я поискал по слову Single и мне открылась вот такая группа методов:
 Согласитесь названия вида SingleOrDefaultOf1IQueryableOfM0ExpressionOfFuncOfM0Boolean впечатляют. Откуда они такие беруться, можно почитать здесь.
Если вы уже читали мою предыдущую статью про модульные тесты, то приведенный фрагмент кода вызовет еще один вопрос: почему это не свойство, а метод. Подтверждения искать мне лень, но я думаю, что это связано с тем, что метод SingleOrDefault является расширяющим. Поэтому мы подмену не присваиваем соответствующему делегату, а передаем в метод. Посмотрев на сигнатуру упомянутого выше метода, мы понимаем, что необходимо в качестве типа Generic-а указать возвращаемый методом тип, ну и передать делегат, который принимает два параметра DbSet и предикат, который отвечает за условие. Т.к. я хотел протестировать ветку когда результат равен null, то написал вот такой код:

System.Linq.Fakes.ShimQueryable.SingleOrDefaultOf1IQueryableOfM0ExpressionOfFuncOfM0Boolean<AccessRequest>(
    (dbSet, predicate) => null
    );
На этом конечно подмены не заканчиваются. Необходимо подменить два конструктора (контекста данных и сервиса BRL):

// Конструктор сервиса
Corp.Life.Geb.Server.Data.Fakes.ShimAccessManagementService.Constructor = s => { };
// Конструктор контекста данных
Corp.Life.Geb.Server.Data.Fakes.ShimAccessManagementModelContainer.Constructor = container => { };

Ну и конечно метод Include. Для моей выборки его пришлось переопределять 3 раза. Какие методы переопределять, можно определить наведя курсор на метод и посмотрев, какие типы передаются в Generic:
В данном случае, это Include>>. В общем, все три переопределения:

System.Data.Entity.Fakes.ShimDbExtensions.IncludeOf2IQueryableOfM0ExpressionOfFuncOfM0M1<AccessRequest, Person>(
    (dbSet, predicate) => null
    );
System.Data.Entity.Fakes.ShimDbExtensions.IncludeOf2IQueryableOfM0ExpressionOfFuncOfM0M1<AccessRequest, IEnumerable<IEnumerable<Stage>>>(
    (dbSet, predicate) => null
    );
System.Data.Entity.Fakes.ShimDbExtensions.IncludeOf2IQueryableOfM0ExpressionOfFuncOfM0M1<AccessRequest, IEnumerable<SoftwareItem>>(
    (dbSet, predicate) => null
    );
Ну и тестирование:

string expected = "Заявка с указанным идентификатором не найдена";
string actual;
AccessManagementService service = new AccessManagementService();
actual = service.RunAccessRequestWorkflow(Guid.NewGuid());
Assert.AreEqual(expected, actual);
На этом можно было бы и остановиться, но. т.к. будут еще тестовые методы, в них также будет необходимо включать FakeContext, переопределять конструкторы и Include, то, вместо Ctrl+C, Ctrl+V, правильнее воспользоваться двумя магическими атрибутами: TestInitialize и TestCleanup. Методы помеченные такими атрибутами (размещены они должны быть в тестовом классе) будут вызываться до и после каждого тестового метода. Т.е. всю инициализацию выносим в первый, закрытие контекста во второй:


[TestInitialize]
public void TestInitialize()
{
    shimContext = ShimsContext.Create();
    // Include
    System.Data.Entity.Fakes.ShimDbExtensions.IncludeOf2IQueryableOfM0ExpressionOfFuncOfM0M1<AccessRequest, Person>(
        (dbSet, predicate) => null
        );
    System.Data.Entity.Fakes.ShimDbExtensions.IncludeOf2IQueryableOfM0ExpressionOfFuncOfM0M1<AccessRequest, IEnumerable<IEnumerable<Stage>>>(
        (dbSet, predicate) => null
        );
    System.Data.Entity.Fakes.ShimDbExtensions.IncludeOf2IQueryableOfM0ExpressionOfFuncOfM0M1<AccessRequest, IEnumerable<SoftwareItem>>(
        (dbSet, predicate) => null
        );
    // Конструктор сервиса
    Corp.Life.Geb.Server.Data.Fakes.ShimAccessManagementService.Constructor = s => { };
    // Конструктор контекста данных
    Corp.Life.Geb.Server.Data.Fakes.ShimAccessManagementModelContainer.Constructor = container => { };
}

[TestCleanup]
public void TestCleanup()
{
    if (shimContext != null)
    {
        shimContext.Dispose();
    }
}
Все.

Комментариев нет:

Отправить комментарий