Название получилось длинное, но как сказать короче, про что сегодня пойдет речь, я не придумал. Итак, под катом, будет:
1. Как создать Fakes Assembly для System.Core.dll (как не странно это звучит, но методы First, Where и другие, да и все пространство System.Linq находиться именно в этой сборке).
2. Как сделать Fake методы для присоединенных методов типа Single и Include.
3. Как вынести инициализацию FakeContext-а и переопределение методов которые не меняются от тестового метода к тестовому методу в отдельный метод, который будет вызываться автоматически.
Начну с небольшой предыстории. У нас месяцев N-цать назад, был написан большой проект. Но то одно, то второе. В общем запускается он со следующего месяца. И тут, в преддверии запуска, у заказчика появляются новые пожелания. Код писался давно, в основном визуалка и несколько методов бизнес-логики вынесенные на сервер. Пожелания затрагивают бизнес-логику, поэтому принято решение, покрыть серверный код модульными тестами.
Немного о структуре проекта:
Клиентская часть написана на SilverLight, сервер состоит из уровня доступа к данным (Data), уровня бизнес-логики (BRL) и Web-проекта, который отвечает за публикацию сервисов. Вот методы BRL проекта и предстоит покрывать тестами.
На этом предыстория заканчивается и начинается история.
Вот фрагмент метода, который предстоит тестировать:
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:
{
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, то написал вот такой код:
Ну и конечно метод Include. Для моей выборки его пришлось переопределять 3 раза. Какие методы переопределять, можно определить наведя курсор на метод и посмотрев, какие типы передаются в Generic:
В данном случае, это Include>>. В общем, все три переопределения:
Чтобы решить проблему, я создал проект по шаблону 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();
}
}
Все.
Комментариев нет:
Отправить комментарий