如何为实体框架 Sql 提供程序编写测试并访问生成的 Sql 命令

Posted

技术标签:

【中文标题】如何为实体框架 Sql 提供程序编写测试并访问生成的 Sql 命令【英文标题】:How to write tests for Entity Framework Sql providers and access the generated Sql command 【发布时间】:2021-11-21 08:28:03 【问题描述】:

我有一个动态 LINQ 库,可以为 anyIQueryable 支持列表和 EntityFramework 生成查询表达式... 我想进行一些测试来检查实体框架 SQL 提供程序生成的 SQL 查询。主要问题是我不能使用In-Memory 提供程序,因为它没有向我显示SQL 输出。在没有安装实际数据库的情况下如何编写这些测试? 有没有我可以使用的包? (我需要测试 EF SqlServer 和 mysql 提供程序)。

【问题讨论】:

【参考方案1】:

使用Interceptor 并禁止实际执行,遵循文档中的此示例:

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)

    if (command.CommandText.StartsWith("-- Get_Daily_Message", StringComparison.Ordinal))
    
        lock (_lock)
        
            if (_message != null
                && DateTime.UtcNow < _queriedAt + new TimeSpan(0, 0, 10))
            
                command.CommandText = "-- Get_Daily_Message: Skipping DB call; using cache.";
                result = InterceptionResult<DbDataReader>.SuppressWithResult(new CachedDailyMessageDataReader(_id, _message));
            
        
    

    return new ValueTask<InterceptionResult<DbDataReader>>(result);

Interceptors - Before Execution

【讨论】:

谢谢你一个小问题,我需要生成迁移才能使用此功能还是无需迁移即可工作? 迁移完全是可选的。很多人从不使用它们,而且这个功能与迁移无关。 这个答案真的很接近我的需要,但没有回答单元测试部分。例如,也许我也应该使用DbConnectionInterceptor 来抑制数据库连接,以及如何从单元测试中访问提供程序生成的命令。等等......无论如何,谢谢大卫,至少现在我知道在哪里寻找和研究。【参考方案2】:

感谢@David Browne - 我最终得到了以下代码。只是想分享一下,如果以后有人遇到类似的问题。

unit test project link

// interceptors.cs
 public class SuppressConnectionInterceptor : DbConnectionInterceptor
   
      public override ValueTask<InterceptionResult> ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData,
         InterceptionResult result,
         CancellationToken cancellationToken = new())
      
         result = InterceptionResult.Suppress();
         return base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken);
      

      public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
      
         result = InterceptionResult.Suppress();
         return base.ConnectionOpening(connection, eventData, result);
      
   

 public class SuppressCommandResultInterceptor : DbCommandInterceptor
   
      public override InterceptionResult<DbDataReader> ReaderExecuting(
         DbCommand command,
         CommandEventData eventData,
         InterceptionResult<DbDataReader> result)
      
         result = InterceptionResult<DbDataReader>.SuppressWithResult(new EmptyMessageDataReader());

         return result;
      

      public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
         DbCommand command,
         CommandEventData eventData,
         InterceptionResult<DbDataReader> result,
         CancellationToken cancellationToken = default)
      
         result = InterceptionResult<DbDataReader>.SuppressWithResult(new EmptyMessageDataReader());

         return new ValueTask<InterceptionResult<DbDataReader>>(result);
      

   public class EmptyMessageDataReader : DbDataReader
   
   
      private readonly List<User> _users = new List<User>();

      public EmptyMessageDataReader()
      
      

      public override int FieldCount
         => 0;

      public override int RecordsAffected
         => 0;

      public override bool HasRows
         => false;

      public override bool IsClosed
         => true;

      public override int Depth
         => 0;

      public override bool Read()
         => false;

      public override int GetInt32(int ordinal)
         => 0;

      public override bool IsDBNull(int ordinal)
         => false;

      public override string GetString(int ordinal)
         => "suppressed message";

      public override bool GetBoolean(int ordinal)
         => true;

      public override byte GetByte(int ordinal)
         => 0;

      public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
         => 0;

      public override char GetChar(int ordinal)
         => '\0';

      public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
         => 0;

      public override string GetDataTypeName(int ordinal)
         => string.Empty;

      public override DateTime GetDateTime(int ordinal)
         => DateTime.Now;

      public override decimal GetDecimal(int ordinal)
         => 0;

      public override double GetDouble(int ordinal)
         => 0;

      public override Type GetFieldType(int ordinal)
         => typeof(User);

      public override float GetFloat(int ordinal)
         => 0;

      public override Guid GetGuid(int ordinal)
         => Guid.Empty;

      public override short GetInt16(int ordinal)
         => 0;

      public override long GetInt64(int ordinal)
         => 0;

      public override string GetName(int ordinal)
         => "";

      public override int GetOrdinal(string name)
         => 0;

      public override object GetValue(int ordinal)
         => new object();

      public override int GetValues(object[] values)
         => 0;

      public override object this[int ordinal]
         => new object();

      public override object this[string name]
         => new object();

      public override bool NextResult()
         => false;

      public override IEnumerator GetEnumerator()
         => _users.GetEnumerator();
   

  
   
// DbContext configuration
   public class MyDbContext : DbContext
   
      public DbSet<User> Users  get; set; 

      protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
      
         optionsBuilder.UseSqlServer("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;");
         optionsBuilder.AddInterceptors(new SuppressCommandResultInterceptor());
         optionsBuilder.AddInterceptors(new SuppressConnectionInterceptor());
         base.OnConfiguring(optionsBuilder);
      
   

// access queryString in unit test
var actual = _dbContext.Users.Where(q => q.Name == "John").ToQueryString();

【讨论】:

以上是关于如何为实体框架 Sql 提供程序编写测试并访问生成的 Sql 命令的主要内容,如果未能解决你的问题,请参考以下文章

我们如何为 64 位操作系统设置 oledb 提供程序

如何为 Linux 应用程序生成证书?

实体框架是不是有内存提供程序?

如何为 Android 应用程序的增强现实编写自动化测试?

我应该如何设置我的集成测试以使用带有实体框架的测试数据库?

我应该如何为测试数据生成具有唯一 id3 数据的 mp3 文件?