Created
May 26, 2022 11:08
-
-
Save GenesisCoast/71e44af277482abf8a1d8ae60680e979 to your computer and use it in GitHub Desktop.
Entity Framework FromSQL() method unit testing original code can be found here https://nodogmablog.bryanhogan.net/2017/11/unit-testing-entity-framework-core-stored-procedures/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Threading; | |
namespace GenesisCoast.Tests.Fakes | |
{ | |
/// <summary> | |
/// Fake implementation of the IAsyncEnumerable so it can be initialized and used/mocked during testing. | |
/// </summary> | |
/// <typeparam name="T">The tpye to add to the internal list.</typeparam> | |
public class AsyncEnumerableFake<T> : IAsyncEnumerable<T>, IQueryable<T> | |
{ | |
/// <summary> | |
/// List of stored procedure items to use. | |
/// </summary> | |
private readonly IAsyncEnumerable<T> storedProcedureItems; | |
/// <summary> | |
/// Initializes a new instance of the <see cref="IAsyncEnumerableFake{T}"/> class. | |
/// </summary> | |
/// <param name="storedProcedureItems">The stored procedure items to pass.</param> | |
public IAsyncEnumerableFake(params T[] storedProcedureItems) | |
{ | |
this.storedProcedureItems = AsyncEnumerable.ToAsyncEnumerable(storedProcedureItems); | |
} | |
/// <inheritdoc/> | |
public Type ElementType => throw new NotImplementedException(); | |
/// <inheritdoc/> | |
public Expression Expression => throw new NotImplementedException(); | |
/// <inheritdoc/> | |
public IQueryProvider Provider => throw new NotImplementedException(); | |
/// <inheritdoc/> | |
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default) | |
{ | |
return this | |
.storedProcedureItems | |
.GetAsyncEnumerator(); | |
} | |
/// <inheritdoc/> | |
public IEnumerator<T> GetEnumerator() | |
{ | |
return this | |
.storedProcedureItems | |
.ToEnumerable() | |
.GetEnumerator(); | |
} | |
/// <inheritdoc/> | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
return this.GetEnumerator(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.EntityFrameworkCore; | |
using GenesisCoast.Data.DataAccessLayer; | |
using System; | |
namespace GenesisCoast.Tests.Helpers | |
{ | |
/// <summary> | |
/// Helper methods related to the Database Context. | |
/// </summary> | |
public static class DatabaseContextTestHelper | |
{ | |
/// <summary> | |
/// Creates a dummy insance of the database context for testing. | |
/// </summary> | |
/// <returns>Database context.</returns> | |
public static StagingDatabaseContext CreateDbContext() | |
{ | |
string databaseName = Guid | |
.NewGuid() | |
.ToString("N"); | |
var builder = new DbContextOptionsBuilder<StagingDatabaseContext>(); | |
var options = builder | |
.UseInMemoryDatabase(databaseName) | |
.Options; | |
return new StagingDatabaseContext(options, true); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.EntityFrameworkCore; | |
using Moq; | |
using GenesisCoast.Tests.Fakes; | |
using System.Diagnostics.CodeAnalysis; | |
using System.Linq; | |
using System.Linq.Expressions; | |
namespace GenesisCoast.Tests.Extensions | |
{ | |
/// <summary> | |
/// Extensions to help with testing a specific database set. | |
/// </summary> | |
public static class DbSetTestExtensions | |
{ | |
/// <summary> | |
/// Mocks the FromSql() method in the DBSet so it can be executed and tested in automated testing. | |
/// </summary> | |
/// <typeparam name="TEntity">The entity that is being queried.</typeparam> | |
/// <param name="dbSet">Database set to extend.</param> | |
/// <param name="queryProvider"> | |
/// Mocked query provider, use this to check how the FromSql() method was actually called. | |
/// </param> | |
/// <param name="items">List of entity items the FromSql() method should return.</param> | |
/// <returns>The mocked database set (DBSet) with the mocked FromSql() function.</returns> | |
[SuppressMessage( | |
"Style", | |
"IDE0060:Remove unused parameter", | |
Justification = "Required for the extension of the DBSet." | |
)] | |
public static DbSet<TEntity> MockFromSql<TEntity>( | |
this DbSet<TEntity> dbSet, | |
out Mock<IQueryProvider> queryProvider, | |
params TEntity[] items | |
) | |
where TEntity : class | |
{ | |
var storedProcedureItems = new StoredProcedureAsyncEnumerableFake<TEntity>(items); | |
var queryProviderMock = new Mock<IQueryProvider>(); | |
queryProviderMock | |
.Setup(p => p.CreateQuery<TEntity>( | |
It.IsAny<MethodCallExpression>()) | |
) | |
.Returns<MethodCallExpression>(x => | |
{ | |
return storedProcedureItems; | |
}); | |
var dbSetMock = new Mock<DbSet<TEntity>>(); | |
dbSetMock | |
.As<IQueryable<TEntity>>() | |
.SetupGet(q => q.Provider) | |
.Returns(() => | |
{ | |
return queryProviderMock.Object; | |
}); | |
dbSetMock | |
.As<IQueryable<TEntity>>() | |
.Setup(q => q.Expression) | |
.Returns(Expression.Constant(dbSetMock.Object)); | |
queryProvider = queryProviderMock; | |
return dbSetMock.Object; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Moq; | |
using System; | |
using System.Linq; | |
using System.Linq.Expressions; | |
namespace GenesisCoast.Tests.Extensions | |
{ | |
/// <summary> | |
/// Provides extensions to help with operations related to a mocked IQueryProvider. | |
/// </summary> | |
public static class IQueryProviderMoqExtensions | |
{ | |
/// <summary> | |
/// Verifies the FromSql() method was correctly called by the IQueryProvider. | |
/// </summary> | |
/// <typeparam name="TEntity">The entity the FromSql() method should return.</typeparam> | |
/// <param name="queryProvider">Query provider to extend.</param> | |
/// <param name="sqlPredicate"> | |
/// Predciate to use when checking the SQL query that is being executed. | |
/// </param> | |
/// <param name="times">Number of times the FromSql() method should be called.</param> | |
public static void VerifyFromSql<TEntity>( | |
this Mock<IQueryProvider> queryProvider, | |
Func<string, bool> sqlPredicate, | |
Times times | |
) | |
{ | |
queryProvider.Verify( | |
v => v.CreateQuery<TEntity>( | |
It.Is<MethodCallExpression>( | |
i => i.Method.Name == "FromSqlOnQueryable" | |
&& sqlPredicate((i.Arguments[1] as ConstantExpression).Value as string) | |
) | |
), | |
times | |
); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Extensions.Logging; | |
using Moq; | |
using GenesisCoast.Tests.Extensions; | |
using GenesisCoast.Tests.Fakes; | |
using NUnit.Framework; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace GenesisCoast.Data.Repositories.Tests | |
{ | |
/// <summary> | |
/// Unit tests for the ProductRepository class. | |
/// </summary> | |
[TestFixture] | |
public class MetricRepositoryTests | |
{ | |
/// <summary> | |
/// Tests the MethodThatCallsFromSql() method returns items. | |
/// </summary> | |
/// <returns>An asynchronous operation.</returns> | |
public async Task TestMethodThatCallsFromSqlReturnsItems() | |
{ | |
using (var context = DatabaseContextTestHelper.CreateDbContext()) | |
{ | |
// Act | |
string productIdOne = Guid | |
.NewGuid() | |
.ToString(); | |
string productIdTwo = Guid | |
.NewGuid() | |
.ToString(); | |
var products = new AsyncEnumerableFake<Product>( | |
new Product() | |
{ | |
ProductId = productIdOne, | |
ProductName = "Some name", | |
Size = 1, | |
Value = 2 | |
}, | |
new Product() | |
{ | |
ProductId = productIdTwo, | |
ProductName = "Some other name", | |
Size = 3, | |
Value = 4 | |
} | |
); | |
context.Products = context | |
.Products | |
.MockFromSql( | |
out Mock<IQueryProvider> queryProvider, | |
products.ToArray() | |
); | |
// Act | |
var results = await productsRepository.MethodThatCallsFromSql(); | |
// Assert | |
queryProvider.VerifyFromSql<Product>( | |
sql => sql.StartsWith("EXEC") | |
&& sql.Contains($"{nameof(Product)}_{nameof(ProductRepository.MethodThatCallsFromSql)}") | |
&& sql.Contains($"@__id = '{productIdOne}'"), | |
Times.Once() | |
); | |
Assert.AreEqual( | |
2, | |
results.Count() | |
); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment