C#: Тестирование Entity Framework из Sql для обеспечения правильного синтаксиса - PullRequest
3 голосов
/ 20 июня 2020

Пишу для проверки FromSql Заявления с базой данных InMemory. Мы пытаемся использовать Sqlite.

Выполнение следующего Sql проходит модульный тест без ошибок.

select * from dbo.Product

Однако выполнение этого также проходит с неправильным синтаксисом sql. Хотите, чтобы тест завершился неудачно из-за неправильного синтаксиса sql. Как мы можем правильно протестировать From Sql?

Ошибка не возникла из-за неправильного синтаксиса.

seledg24g5ct * frofhm dbo.Product

Полный код:

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);
        }

Использование Net Core 3.1

Ответы [ 2 ]

6 голосов
/ 19 августа 2020

Здесь следует принять во внимание две вещи.

Во-первых, метод FromSql - это всего лишь крошечный мост для использования необработанных SQL запросов в EF Core. При вызове метода никакой проверки / анализа переданной строки SQL не происходит, за исключением поиска заполнителей параметров и связывания с ними параметров db. Чтобы пройти проверку, он должен быть выполнен .

Во-вторых, для поддержки композиции запроса по набору результатов FromSql метод возвращает IQueryable<T>. Это означает, что он выполняется не сразу, а только в том случае, если / когда результат перечислен. Это может произойти, если вы используете foreach l oop поверх него или вызываете такие методы, как ToList, ToArray или метод расширения EF Core c Load, который похож на ToList, но без создание списка - эквивалент foreach l oop без тела, например

foreach (var _ in query) { }

При этом, фрагмент кода

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

практически ничего не делает, следовательно не создает исключения для недопустимого SQL. Вы должны выполнить его, используя один из вышеупомянутых методов, например,

productList.Load();

или

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

и подтвердить ожидаемое исключение.

Для получения дополнительной информации см. Необработанные SQL Запросы и Как работают запросы разделы документации EF Core.

0 голосов
/ 25 августа 2020

@ ivan-stoev ответил на ваш вопрос о том, почему ваш оператор '.From Sql' ничего не делает, т.е. запрос никогда не материализуется. Но чтобы попытаться добавить немного дополнительной ценности, я поделюсь своей настройкой модульного теста, поскольку она мне подходит. Конечно, YMMV.

  1. Создайте повторно используемый класс для обработки общих c баз данных в памяти и простого заполнения таблиц тестовыми данными. NB: для этого требуются пакеты Nuget:
  • ServiceStack.OrmLite.Core
  • ServiceStack.OrmLite.Sqlite

Я использую OrmLite как он позволяет имитировать и модульное тестирование, предоставляя фабрику соединений без удаления, которую я могу аккуратно внедрить в классы Test через 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);
                }
            }
        }
    }
Класс 'DbConnectionManager<EFContext>' для предоставления оболочки для соединения с базой данных с использованием контекста EF, который вы уже создали. Это захватывает соединение с базой данных из контекста EF и абстрагирует операции открытия / закрытия:
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();
                }
            }
        } 

Сопутствующий вводимый интерфейс для вышеуказанного:

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

        void Dispose();
    }
В вашем. NET классе запуска проекта зарегистрируйте этот интерфейс с помощью встроенного контейнера DI (или любого другого, который вы используете):
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(typeof(IDbConnectionManager<>), typeof(DbConnectionManager<>));
}
Теперь наш класс Unit Test выглядит так:
/// <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);
        }
    }

Примечания к вышеизложенному:

  • Вы можете видеть, что я тестирование класса доступа к данным «ProductAccess», который охватывает мои вызовы базы данных, но его должно быть достаточно легко изменить для вашей настройки. Мой класс ProductAccess ожидает, что будут введены другие службы, такие как ведение журнала и конфигурация, но я закомментировал их для этого минимального примера.
  • Обратите внимание на настройку базы данных в памяти и заполнение ее списком тестов сущностей для запроса теперь представляет собой простые 2 строки (вы даже можете сделать это только один раз в конструкторе класса Test, если хотите, чтобы один и тот же набор тестовых данных использовался во всех тестах):

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

...