Статьи

Смерть DAO и как проверить LINQ

Изредка я слышу жалобы, что LINQ сложно провести модульное тестирование. Заметьте, что эти жалобы касаются не LINQ-объектов, они связаны со сложностями ароматов LINQ, которые превращают код C # в нечто другое, например SQL или CAML, используя деревья выражений . Наиболее распространенными технологиями являются LINQ to SQL, Entity Framework или, в моем случае, LINQ to SharePoint. В этой статье я собираюсь предложить метод, который делает тестирование LINQ не просто, но и элегантным — при условии, что у вас все в порядке с методами расширения — множество методов расширения. И при условии, что вы готовы убить свой уровень объектов доступа к данным (DAO).

Проблема модульного тестирования

Любая архитектура нуждается в месте для размещения кода, который находит объекты. Например, FindBySocialSecurityNumber (). В традиционной архитектуре мы могли бы поместить такой метод на уровень DAO. Если так, то наш метод будет выглядеть примерно так:

public class  EmployeesDao {
public Employee FindBySSN(Context ctx, string ssn) {
return ctx.Employees.SingleOrDefault(e => e.Ssn == ssn);
}
}

Итак, как бы мы пошли об этом модульном тестировании?

Одним из довольно типичных решений было бы использование базы данных в памяти. Этот подход работает, если наше хранилище данных является базой данных, но, безусловно, не работает, если хранилище данных является чем-то менее традиционным, как SharePoint. Но даже если наше хранилище является базой данных, у нас все равно будут проблемы с настройкой базы данных в памяти.

Другим решением может быть использование фиктивного контекста, который возвращает IQueryable. Но разве не было бы замечательно, если бы мы могли избежать насмешек?

Убийство ДАО

Первый вопрос: почему у нас даже есть уровень DAO для начала. Первоначальная идея заключалась в том, что мы хотели найти место для кода, специфичного для конкретного хранилища данных. Другими словами, мы хотели изолировать код, который необходимо будет изменить, если хранилище данных перейдет с SQL Server на Oracle. Но разве это не то, что делает LINQ? Я был бы очень удивлен, если бы не было приличного поставщика LINQ для любого хранилища данных, которое требовало бы более минимальных изменений кода. Так почему бы не принять LINQ и пересмотреть альтернативы уровню DAO?

Одна альтернатива, которую я использую уже более месяца, — это переключение на методы расширения. Для того, чтобы дать кредит , где это связано идея возникла с разговора с собрат Near Бесконечность сотрудника Джо Фернер . И я уверен, что идея не особенно оригинальна (пожалуйста, оставьте в комментариях, если вы знаете других, которые используют этот подход).

Используя эту технику, наш код меняется примерно так:

var employeeDao = new EmployeesDao(); // or use IOC of course
employeeDao.FindBySSN(ctx, "111-11-1111");

Чтобы что-то вроде этого:

ctx.Employees.FindBySSN("111-11-1111");

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

Мы могли бы реализовать это вне свойства Employees контекста, если бы у нас был контроль над этим (чего я не делаю с spmetal). Но если мы реализуем это как метод расширения вроде этого:

public static class EmployeeExtensions {
public static Employee FindBySSN(this IQueryable<Employee> employees, string ssn) {
return employees.SingleOrDefault(e => e.Ssn == ssn);
}
}

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

Тестирование Это

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

public void FindBySSN_OneSsnExists_EmployeeReturned() {
var employees = new [] { new Employee { Ssn = "111-11-1111" } };
var actual = employees.AsQueryable().FindBySSN("111-11-1111");
Assert.IsNotNull(actual);
}

Обратите внимание, что нам не нужно было насмехаться.

Тестируемость, но какой ценой?

Этот метод отлично работает для приведенного выше примера, но как он масштабируется, чтобы обострить проблемы и каковы другие недостатки?

Что касается масштабируемости, я обнаружил, что эта техника отлично работает для каждого сценария, с которым я столкнулся в течение месяца, в котором я ее делал. Он работает для объединений, объединений и даже для вставок, обновления и удаления.

Что касается недостатков, то проницательный читатель может задаться вопросом о насмешливости. Например, что, если мы хотим смоделировать вызов FindBySSN и дать ему точного сотрудника, который будет возвращен. Этот сценарий, по общему признанию, сложнее. Но я обнаружил, что гораздо чаще мне не нужно высмеивать типы вещей, которые раньше жили на уровне DAO. Вместо этого я просто высмеиваю объект Employee из контекста, чтобы вернуть объекты в памяти и сделать мои тесты немного больше по объему. Большую часть времени я нахожу, что большая область увеличивает полезность теста. В тех случаях, когда я действительно хочу издеваться над уровнем «DAO», я использую технику, описанную в этом посте Даниэлем Каззулино.

Вывод

Очевидно, что в этой архитектуре есть нечто большее, например, как вы обрабатываете операции вставки и обновления? Короткий ответ — это легко, но я сохраню эту тему для будущего поста. А пока почему бы не попробовать этот подход? Вы все равно не были довольны этим бесполезным старым уровнем DAO? Я говорю, что мы искореним это и никогда не оглядываемся назад.