Вышла новая версия LinqTestable — библиотеки для тестирования запросов к бд через ORM

от автора

LinqTestable — это библиотека, помогающая преодолеть в тестах концептуальный разрыв между ООП и реляционной БД, возникающий из-за разницы поведения NULL-а в этих двух парадигмах. Например, сравнение NULL == NULL возвращает истину в объектных языках, и ложь в реляционной модели. Помимо этого, NULL.SomeField вернёт NULL в реляционной модели и выбросит NullReferenceException в C#. LinqTestable предназначена для решения этой проблемы.


Сразу скажу, что библиотека ещё не до конца готова, но в принципе пользоваться уже можно. В настоящий момент я работаю над корректной обработкой OrderBy.

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

Устранение NullRefenceException

Следующий пример демонстрирует устранение проблемы выбрасывания NullRefenceException в момент обращения к null.DOOR_ID; вместо этого корректно возвращается null.

Код теста

    [TestFixture]     public class TwoLeftJoins     {         void ExecuteTwoLeftJoins(bool isSmart)         {             var dataModel = new TestDataModel {Settings = {IsSmart = isSmart}};              const int carId = 100;             dataModel.CAR.AddObject(new CAR{CAR_ID = carId});             dataModel.CAR.AddObject(new CAR{CAR_ID = carId + 1});              var cars =                 (from car in dataModel.CAR                 join door in dataModel.DOOR on car.CAR_ID equals door.CAR_ID                      into joinedDoor from door in joinedDoor.DefaultIfEmpty()                 join doorHandle in dataModel.DOOR_HANDLE on door.DOOR_ID equals doorHandle.DOOR_ID                      into joinedDoorHandle from doorHandle in joinedDoorHandle.DefaultIfEmpty()                 select car).ToList();              Assert.AreEqual(2, cars.Count);             Assert.AreEqual(carId, cars.First().CAR_ID);         }          [Test]         public void TwoLeftJoinsShouldThrow()         {             Assert.Throws<NullReferenceException>(() => ExecuteTwoLeftJoins(false));         }          [Test]         public void SmartTwoLeftJoinsShouldNotThrow()         {             ExecuteTwoLeftJoins(true);         }     } 

Для предотвращения NullReferenceException можно было бы вручную добавить в код проверку на null, но в таком случае вы рискуете получить другой SQL и план исполнения запроса, отличающийся от изначального. Об этом я писал в прошлой статье, посвященной этой библиотеке.

Случай, похожий на предыдущий, однако на этот раз NullReferenceException предотвращен в момент обращения к Value у Nullable:

Код теста

    [TestFixture]     public class Contains     {         public void ExecuteContains(bool isSmart)         {             var dataModel = new TestDataModel { Settings = { IsSmart = isSmart } };              new[]             {                 new DOOR_HANDLE {DOOR_HANDLE_ID = 1, MATERIAL_ID = 1},                 new DOOR_HANDLE {DOOR_HANDLE_ID = 2, MATERIAL_ID = 2},                 new DOOR_HANDLE {DOOR_HANDLE_ID = 3, MATERIAL_ID = null}             }                 .ForEach(dataModel.DOOR_HANDLE.AddObject);                         var doorHandleIds = new List<int>{1,2};              var doorHandles =                 (from doorHandle in dataModel.DOOR_HANDLE                 where doorHandleIds.Contains(doorHandle.MATERIAL_ID.Value)                 select doorHandle).ToList();              Assert.AreEqual(2, doorHandles.Count);         }          [Test]         public void ContainsShouldFail()         {             Assert.Throws<InvalidOperationException>(() => ExecuteContains(false));         }          [Test]         public void SmartContainsShouldSuccess()         {             ExecuteContains(true);         }     } 

Корректное поведение Sum

Метод Sum ведёт себя по-разному в базе данных и в C# над списком из данных. В случае суммы от пустой выборки, база данных возвращает NULL, а в С# возвращается 0. Если поле, в которое передаётся результат суммы, было не Nullable<>, то ORM выбрасывает исключение. При использовании LinqTestable, Sum или вернёт NULL, или выбросит исключение, точно также, как если бы вы использовали базу данных:

Код теста

    [TestFixture]     public class SumFromEmptyTable     {         void ExecuteSumFromEmptyTable(bool isSmart)         {             var dataModel = new TestDataModel {Settings = {IsSmart = isSmart}};             int sum = dataModel.CAR.Sum(x => x.CAR_ID);         }          [Test]         public void SmartSumShouldThrow()         {             Assert.Throws<InvalidOperationException>(() => ExecuteSumFromEmptyTable(true));         }          [Test]         public void SumShouldNotThrow()         {             ExecuteSumFromEmptyTable(false);         }          void ExecuteNullableSumFromEmptyTable(bool isSmart)         {             var dataModel = new TestDataModel { Settings = { IsSmart = isSmart } };             int? sum = dataModel.DOOR_HANDLE.Sum(x => x.MATERIAL_ID);             Assert.AreEqual(null, sum);         }          [Test]         public void NullableSumShouldFail()         {             Assert.Throws<AssertionException>(() => ExecuteNullableSumFromEmptyTable(false));         }          [Test]         public void NullableSmartSumShouldSuccess()         {             ExecuteNullableSumFromEmptyTable(true);         }     } 

Сравнение null == null

При использовании LinqTestable, null == null будет означать false, если только вы явно в запросе не делаете сравнение чего то с null, точно также, как если бы вы делали запрос к базе данных:

Код теста

    [TestFixture]     public class NullComparison     {         void ExecuteNullComparison(bool isSmart)         {             var dataModel = new TestDataModel { Settings = { IsSmart = isSmart } };              new[]             {                 new DOOR_HANDLE {DOOR_HANDLE_ID = 1, MATERIAL_ID = 1, MANUFACTURER_ID = 1}, // <----                 new DOOR_HANDLE {DOOR_HANDLE_ID = 2, MATERIAL_ID = 2, MANUFACTURER_ID = 2}, //      |-- this is only pair                 new DOOR_HANDLE {DOOR_HANDLE_ID = 3, MATERIAL_ID = 1, MANUFACTURER_ID = 1}, // <----                 new DOOR_HANDLE {DOOR_HANDLE_ID = 4, MATERIAL_ID = 5, MANUFACTURER_ID = null},                 new DOOR_HANDLE {DOOR_HANDLE_ID = 5, MATERIAL_ID = 5, MANUFACTURER_ID = null},                 new DOOR_HANDLE {DOOR_HANDLE_ID = 6, MATERIAL_ID = null, MANUFACTURER_ID = null},                 new DOOR_HANDLE {DOOR_HANDLE_ID = 7, MATERIAL_ID = null, MANUFACTURER_ID = null}             }             .ForEach(x => dataModel.DOOR_HANDLE.AddObject(x));              var handlePairsWithSameMaterialAndManufacturer =                (from handle in dataModel.DOOR_HANDLE                 join anotherHandle in dataModel.DOOR_HANDLE on handle.MATERIAL_ID equals anotherHandle.MATERIAL_ID                 where handle.MANUFACTURER_ID == anotherHandle.MANUFACTURER_ID && handle.DOOR_HANDLE_ID < anotherHandle.DOOR_HANDLE_ID                 select new {handle, anotherHandle}).ToList();              Assert.AreEqual(1, handlePairsWithSameMaterialAndManufacturer.Count);             var pair = handlePairsWithSameMaterialAndManufacturer.First();             Assert.AreEqual(1, pair.handle.MATERIAL_ID);             Assert.AreEqual(pair.handle.MATERIAL_ID, pair.anotherHandle.MATERIAL_ID);             Assert.AreEqual(1, pair.handle.MANUFACTURER_ID);             Assert.AreEqual(pair.handle.MANUFACTURER_ID, pair.anotherHandle.MANUFACTURER_ID);         }          [Test]         public void NullComparisonShouldFail()         {             Assert.Throws<AssertionException>(() => ExecuteNullComparison(false));         }          [Test]         public void SmartNullComparisonShouldSuccess()         {             ExecuteNullComparison(true);         }     } 

Как начать использовать

Библиотека относится к свободному ПО и поставляется «как есть» (as is, no warranty). Можно скачать через Nuget.

После подключения библиотеки, замените в вашем тестовом ObjectSet реализацию свойств Expression и Provider на:

        public System.Linq.Expressions.Expression Expression         {             get { return _collection.AsQueryable<T>().ToTestable().Expression; }         }          public IQueryProvider Provider         {             get { return _collection.AsQueryable<T>().ToTestable().Provider; }         } 
Можно добавить переключатель для включения\отключения LinqTestable

        public Expression Expression         {             get             {                 return                      _settings.IsSmart ?                     _collection.AsQueryable().ToTestable().Expression :                      _collection.AsQueryable().Expression;             }         }          public IQueryProvider Provider         {             get {                      return                         _settings.IsSmart ?                         _collection.AsQueryable().ToTestable().Provider :                          _collection.AsQueryable().Provider;               }         } 

Можете посмотреть пример, как реализована тестовая база данных и к ней подключена LinqTestable в тестах самой библиотеки. Вот исходники.

На всякий случай, вот статья о том, как реализовать тестовую базу данных на примере Entity Framework.

На данный момент библиотека не умеет работать с сортировкой (OrderBy), есть несколько других мелких недочётов, которые планируется исправить в ближайшем будущем. Также собираюсь немного порефачить код.

Если вы обнаружите какие-либо баги или случаи, когда поведение в бд и в C# отличается, которые не обрабатываются библиотекой — буду признателен, если вы пришлёте проблемный unit-тест на почту LinqTestable@mail.ru

ссылка на оригинал статьи http://habrahabr.ru/post/273801/