3

I am writing to test FromSql Statement with InMemory Database. We are attempting to utilize Sqlite.

Running the following Sql passes the unit test without error.

select * from dbo.Product

However, doing this also passes with incorrect sql syntax. Would like to make the test fail with improper sql syntax. How can we test FromSql properly?

No error came from result of bad syntax .

seledg24g5ct * frofhm dbo.Product

Full Code:

namespace Tests.Services
{
    public class ProductTest
    {
        private const string InMemoryConnectionString = "DataSource=:memory:";
        private SqliteConnection _connection;
        protected TestContext testContext;

        public ProductServiceTest()
        {
            _connection = new SqliteConnection(InMemoryConnectionString);
            _connection.Open();
            var options = new DbContextOptionsBuilder<TestContext>()
                    .UseSqlite(_connection)
                    .Options;
            testContext= new TestContext(options);
            testContext.Database.EnsureCreated();
        }


        [Fact]
        public async Task GetProductByIdShouldReturnResult()
        {
            var productList = testContext.Product
    .FromSql($"seledg24g5ct * frofhm dbo.Product");

            Assert.Equal(1, 1);
        }

Using Net Core 3.1

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • `Assert.Equal(1, 1);` - will always pass ;) – Fabio Jun 20 '20 at 07:19
  • yeah, how do I make sure the syntax will fail? Usually it will error out before it reaches that assert ? –  Jun 20 '20 at 07:20
  • _Would like to make the test fail with improper sql syntax_ - run tests against actual sql database. – Fabio Jun 20 '20 at 07:20
  • I am trying to prevent localhost, since this is an inmemory test, does sqlite not enforce that? https://github.com/dotnet/efcore/issues/7212 –  Jun 20 '20 at 07:22
  • "In-memory" provider even in Sqlite is not an actual sql engine - if you want to test raw sql query run it against actual sql engine. – Fabio Jun 20 '20 at 07:28
  • ok, wish someone created an inmemory item with a sql engine mock, thanks –  Jun 20 '20 at 07:31
  • At this day, we can afford to have local database on developer machine, we can have database installed on CI server. Only you need to run those tests synchronously and accept that they wouldn't be fast as other tests. There is always trade-off - just select one – Fabio Jun 20 '20 at 07:38
  • yeah true, just our deveps department is overbooked, we have to fill out approval papers to create localdbs on 2 environments for public builds, may take 6 weeks, then setup access logins, etc –  Jun 20 '20 at 07:41
  • Depending on how SQLite-specific your tests are, you might find that Microsoft's SqlParser package would suffice. I [blogged about it](https://blog.wiseowls.co.nz/index.php/2020/05/26/t-sql-syntax-analysis/) a little while ago (shameless self-promotion here), maybe your scenario would be as easy as try to parse the text and catch exceptions. This way no actual DB would be needed – timur Aug 18 '20 at 22:34
  • if you only have to check valid SQL syntax look at this answer https://stackoverflow.com/a/6287892/607245 – gidanmx2 Aug 19 '20 at 09:45

2 Answers2

5

There are two things to be taken into consideration here.

First, FromSql method is just a tiny bridge for using raw SQL queries in EF Core. No any validation/parsing of the passed SQL string occurs when the method is called except finding the parameter placeholders and associating db parameters with them. In order to get validated, it has to be executed.

Second, in order to support query composition over the FromSql result set, the method returns IQueryable<T>. Which means it is not executed immediately, but only if/when the result is enumerated. Which could happen when you use foreach loop over it, or call methods like ToList, ToArray or EF Core specific Load extension method, which is similar to ToList, but without creating list - the equivalent of foreach loop w/o body, e.g.

foreach (var _ in query) { }

With that being said, the code snippet

var productList = testContext.Product
    .FromSql($"seledg24g5ct * frofhm dbo.Product");

does basically nothing, hence does not produce exception for invalid SQL. You must execute it using one of the aforementioned methods, e.g.

productList.Load();

or

var productList = testContext.Product
    .FromSql($"seledg24g5ct * frofhm dbo.Product")
    .ToList();

and assert the expected exception.

For more info, see Raw SQL Queries and How Queries Work sections of EF Core documentation.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
0

@ivan-stoev has answered your question as to why your '.FromSql' statement does nothing - i.e. the query is never actually materialized. But to try and add some additional value, i'll share my Unit Test setup as it works well for me. Of course, YMMV.

  1. Create a reusable class to handle generic In-memory database creation and easy population of tables with test data. NB: this requires the Nuget packages:
  • ServiceStack.OrmLite.Core
  • ServiceStack.OrmLite.Sqlite

I am using OrmLite as it allows for mocking and unit testing by providing a non-disposing connection factory which I can neatly inject into the Test classes via Dependency Injection:

/// <summary>
    /// It is not possible to directly mock the Dapper commands i'm using to query the underlying database. There is a Nuget package called Moq.Dapper, but this approach doesnt need it.
    /// It is not possible to mock In-Memory properties of a .NET Core DbContext such as the IDbConnection - i.e. the bit we actually want for Dapper queries.
    /// for this reason, we need to use a different In-Memory database and load entities into it to query. Approach as per: https://mikhail.io/2016/02/unit-testing-dapper-repositories/
    /// </summary>
    public class TestInMemoryDatabase
    {
        private readonly OrmLiteConnectionFactory dbFactory =
            new OrmLiteConnectionFactory(":memory:", SqliteDialect.Provider);

        public IDbConnection OpenConnection() => this.dbFactory.OpenDbConnection();

        public void Insert<T>(IEnumerable<T> items)
        {
            using (var db = this.OpenConnection())
            {
                db.CreateTableIfNotExist<T>();
                foreach (var item in items)
                {
                    db.Insert(item);
                }
            }
        }
    }
  1. A 'DbConnectionManager<EFContext>' class to provide the wrapper to the database connection using the EF Context you will already have created. This grabs the database connection from the EF Context and abstracts away the opening/closing operations:
public class DbConnectionManager<TContext> : IDbConnectionManager<TContext>, IDisposable
            where TContext : DbContext
        {
            private TContext _context;
    
            public DbConnectionManager(TContext context)
            {
                _context = context;
            }
    
            public async Task<IDbConnection> GetDbConnectionFromContextAsync()
            {
                var dbConnection = _context.Database.GetDbConnection();
    
                if (dbConnection.State.Equals(ConnectionState.Closed))
                {
                    await dbConnection.OpenAsync();
                }
    
                return dbConnection;
            }
    
            public void Dispose()
            {
                var dbConnection = _context.Database.GetDbConnection();
    
                if (dbConnection.State.Equals(ConnectionState.Open))
                {
                    dbConnection.Close();
                }
            }
        } 

Accompanying injectable Interface for the above:

public interface IDbConnectionManager<TContext>
        where TContext : DbContext
    {
        Task<IDbConnection> GetDbConnectionFromContextAsync();

        void Dispose();
    }
  1. In your .NET Project Startup class, register this interface with the inbuilt DI container (or whatever one you're using):
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(typeof(IDbConnectionManager<>), typeof(DbConnectionManager<>));
}
  1. Now our Unit Test class looks like this:
/// <summary>
    /// All tests to follow the naming convention: MethodName_StateUnderTest_ExpectedBehaviour
    /// </summary>
    [ExcludeFromCodeCoverage]
    public class ProductTests
    {
        //private static Mock<ILoggerAdapter<Db2DbViewAccess>> _logger;
        //private static Mock<IOptions<AppSettings>> _configuration;
        private readonly Mock<IDbConnectionManager<Db2Context>> _dbConnection;

        private readonly List<Product> _listProducts = new List<Product>
        {
            new Product
            {
                Id = 1,
                Name = "Product1"
            },
            new Product
            {
                Id = 2,
                Name = "Product2"
            },
            new Product
            {
                Id = 3,
                Name = "Product3"
            },
        };

        public ProductTests()
        {
            //_logger = new Mock<ILoggerAdapter<Db2DbViewAccess>>();
            //_configuration = new Mock<IOptions<AppSettings>>();
            _dbConnection = new Mock<IDbConnectionManager<Db2Context>>();
        }

        [Fact]
        public async Task GetProductAsync_ResultsFound_ReturnListOfAllProducts()
        {
            // Arrange
            // Using a SQL Lite in-memory database to test the DbContext. 
            var testInMemoryDatabase = new TestInMemoryDatabase();
            testInMemoryDatabase.Insert(_listProducts);

            _dbConnection.Setup(c => c.GetDbConnectionFromContextAsync())
                .ReturnsAsync(testInMemoryDatabase.OpenConnection());

            //_configuration.Setup(x => x.Value).Returns(appSettings);

            var productAccess = new ProductAccess(_configuration.Object); //, _logger.Object, _dbConnection.Object);

            // Act
            var result = await productAccess.GetProductAsync("SELECT * FROM Product");

            // Assert
            result.Count.Should().Equals(_listProducts.Count);
        }
    }

Notes on the above:

  • You can see i'm testing a 'ProductAccess' Data Access class which wraps my database calls but that should be easy enough to change for your setup. My ProductAccess class is expecting other services such as logging and Configuration to be injected in, but i have commented these out for this minimal example.
  • Note the setup of the in-memory database and populating it with your test list of entities to query is now a simple 2 lines (you could even do this just once in the Test class constructor if you want the same test dataset to use across tests):

var testInMemoryDatabase = new TestInMemoryDatabase(); testInMemoryDatabase.Insert(_listProducts);

Ciaran
  • 543
  • 5
  • 14