Skip to content

Instantly share code, notes, and snippets.

@GenesisCoast
Created May 26, 2022 11:08
Show Gist options
  • Save GenesisCoast/71e44af277482abf8a1d8ae60680e979 to your computer and use it in GitHub Desktop.
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/
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();
}
}
}
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);
}
}
}
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;
}
}
}
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
);
}
}
}
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