- PVSM.RU - https://www.pvsm.ru -
LinqTestable — это библиотека, помогающая преодолеть в тестах концептуальный разрыв между ООП и реляционной БД, возникающий из-за разницы поведения NULL-а в этих двух парадигмах. Например, сравнение NULL == NULL возвращает истину в объектных языках, и ложь в реляционной модели. Помимо этого, NULL.SomeField вернёт NULL в реляционной модели и выбросит NullReferenceException в C#. LinqTestable предназначена для решения этой проблемы.
Сразу скажу, что библиотека ещё не до конца готова, но в принципе пользоваться уже можно. В настоящий момент я работаю над корректной обработкой OrderBy.
Для демонстрации возможностей библиотеки, готовых на данный момент, приведу примеры некоторых юнит-тестов, которые также можно посмотреть в исходниках [1] библиотеки.
Следующий пример демонстрирует устранение проблемы выбрасывания 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 и план исполнения запроса, отличающийся от изначального. Об этом я писал в прошлой статье, посвященной этой библиотеке [2].
Случай, похожий на предыдущий, однако на этот раз 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 ведёт себя по-разному в базе данных и в 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);
}
}
При использовании 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; }
}
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 в тестах самой библиотеки. Вот исходники [1].
На всякий случай, вот статья [3] о том, как реализовать тестовую базу данных на примере Entity Framework.
На данный момент библиотека не умеет работать с сортировкой (OrderBy), есть несколько других мелких недочётов, которые планируется исправить в ближайшем будущем. Также собираюсь немного порефачить [4] код.
Если вы обнаружите какие-либо баги или случаи, когда поведение в бд и в C# отличается, которые не обрабатываются библиотекой — буду признателен, если вы пришлёте проблемный unit-тест на почту LinqTestable@mail.ru
Автор: FiresShadow
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/tdd/107414
Ссылки в тексте:
[1] исходниках: https://github.com/FiresShadow/LinqTestable
[2] прошлой статье, посвященной этой библиотеке: http://habrahabr.ru/post/269917/
[3] статья: http://www.codeproject.com/Articles/447988/How-to-Mock-Test-an-Entity-Framework-Model-First-P
[4] порефачить: https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D1%84%D0%B0%D0%BA%D1%82%D0%BE%D1%80%D0%B8%D0%BD%D0%B3
[5] Источник: http://habrahabr.ru/post/273801/
Нажмите здесь для печати.