📜 ⬆️ ⬇️

A new version of LinqTestable has been released - libraries for testing requests to the database via ORM

LinqTestable is a library that helps to overcome in tests a conceptual gap between OOP and a relational database arising from the difference in NULL behavior in these two paradigms. For example, comparing NULL == NULL returns true in object languages, and false in the relational model. In addition, NULL.SomeField will return NULL in the relational model and throw a NullReferenceException in C #. LinqTestable is designed to solve this problem.




I’ll say right away that the library is not yet fully prepared, but in principle it is already possible to use it. I am currently working on processing OrderBy correctly.
')
To demonstrate the capabilities of the library that are currently ready, I will give examples of some unit tests, which can also be viewed in the source code of the library.

Eliminate NullRefenceException



The following example demonstrates how to eliminate the problem of throwing a NullRefenceException at the time of a call to null.DOOR_ID; instead, it returns null correctly.
Test code
[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); } } 



To prevent a NullReferenceException, you could manually add a null check to the code, but in this case, you risk getting another SQL and a query execution plan that is different from the original one. I wrote about this in the last article devoted to this library .

A case similar to the previous one, however, this time NullReferenceException is prevented when Nullable accesses Value:
Test code
  [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 correct behavior



The Sum method behaves differently in the database and in C # above the list of data. In the case of a sum from an empty sample, the database returns NULL, and 0 is returned in C #. If the field to which the result of the sum is transferred was not Nullable <>, then ORM throws an exception. When using LinqTestable, Sum will either return NULL or throw an exception, just as if you were using the database:
Test code
  [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); } } 



Comparison null == null



When using LinqTestable, null == null will mean false unless you explicitly make a comparison of something with null in the query, just as if you were making a query to the database:
Test code
  [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); } } 



How to start using



The library refers to free software and is supplied “as is” (as is, no warranty). You can download via Nuget.



After connecting the library, replace in your test ObjectSet the implementation of the Expression and Provider properties with:

  public System.Linq.Expressions.Expression Expression { get { return _collection.AsQueryable<T>().ToTestable().Expression; } } public IQueryProvider Provider { get { return _collection.AsQueryable<T>().ToTestable().Provider; } } 


You can add a switch to enable / disable 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; } } 



You can see an example of how the test database is implemented and LinqTestable is connected to it in the tests of the library itself. Here are the sources .

Just in case, here’s an article on how to implement a test database using the Entity Framework as an example.

At the moment, the library does not know how to work with sorting (OrderBy), there are several other minor issues that are planned to be fixed in the near future. I'm also going to revise the code.

If you find any bugs or cases where the behavior in the database and in C # is different, which are not handled by the library - I would be grateful if you send a problem unit test to LinqTestable@mail.ru

Source: https://habr.com/ru/post/273801/


All Articles