模拟数据库事务?

Posted

技术标签:

【中文标题】模拟数据库事务?【英文标题】:Mocking Database transactions? 【发布时间】:2014-01-15 10:58:35 【问题描述】:

我有一对具有父/子关系的表 - 事件和事件详细信息。我有一个包含来自这两个表的信息的视图模型。我有一个业务层方法,它传递了一个需要更新两个表的视图模型实例。

所以,在方法中,我使用的是EF6的新事务机制:

using (var transaction = this.db.Database.BeginTransaction())

    try
    
        // various database stuff
        this.db.SaveChanges();
        // more database stuff
        this.db.SaveChanges();
        // yet more database stuff
        this.db.SaveChanges();

        transaction.Commit();
    
    catch (Exception ex)
    
        transaction.Rollback();
        this.logger.logException(ex, "Exception caught in transaction, rolling back");
        throw;
    

所以,我的问题。我该如何测试?

我正在使用 Microsoft 的单元测试框架和 Moq,我在模拟 DBContexts 和 DbSets 时没有遇到任何问题,但我似乎无法弄清楚如何绕过事务。

如果我不尝试模拟交易,我会收到 InvalidOperationException:

"在应用程序中找不到名为xxx的连接字符串 配置文件。”

这很有意义 - 没有应用程序配置文件,也没有任何数据库。

但如果我尝试模拟 BeginTransaction(),我会收到初始化错误:NotSupportedException:

"非虚拟成员上的无效设置:m => m.Database.BeginTransaction”。

这让我陷入了困境,查看了 .NET 方法的反编译,试图识别一些可能从可用接口派生的类,或者我可以以某种方式注入模拟对象的东西。

我并不是要对 MS 的事务代码进行单元测试——我只是想确保对每个表中的相应记录进行了适当的更改。但就目前而言,它看起来是不可测试的,并且任何使用事务的方法都是不可测试的。这只是一种痛苦。

我用谷歌搜索了一遍,没有发现任何有用的东西。有没有人遇到过这个问题?有人对如何进行有想法吗?

【问题讨论】:

***.com/questions/32554029/… 【参考方案1】:

测试这类东西总是很复杂,但首先你应该问自己是要对业务逻辑进行单元测试,还是要对应用程序进行集成测试。

如果你想对你的逻辑进行单元测试,你基本上不应该尝试模拟实体框架,因为你不想测试 EF,你只想测试你的代码,对吧? 为此,请模拟任何数据访问对象并仅对您的业务逻辑进行单元测试。

但是如果你想测试你的数据访问层是否工作,例如如果您的代码可以处理您已实现的所有 CRUD 操作,您应该针对真实数据库进行集成测试。在这种情况下,不要尝试模拟任何数据访问对象 (EF),只需针对测试数据库或 sql-express localDB 运行测试即可。

【讨论】:

问题是这个方法需要在单个事务中进行一系列数据库调用。我要测试的是查看是否进行了适当的调用。我愿意假设当我插入或更新记录时,会对数据库进行适当的更改。我不愿意假设的是,当我调用该方法时,会进行正确的插入或更新系列。 @JeffDege,您描述的是集成测试,而不是单元测试。单元测试不应该依赖于任何数据库。为这些东西写一个集成测试。 如果我在与数据库交谈,是的,这将是一个集成测试。这就是我试图模拟数据库的原因。 我开始明白 EF 不是模拟友好的。但我强烈反对这样做是有充分理由的。 我在这里 100% 同意 Jeff。我在事务中运行存储过程,然后使用存储过程的输出参数执行业务逻辑。我需要测试该业务逻辑,但我不能因为 EF 中的事务设置方式?那是垃圾。【参考方案2】:

您可以将上下文和事务包装在一个接口中,然后通过某个提供程序类来实现该接口:

public interface IDbContextProvider

    YourContext Context  get; set; 
    DbContextTransaction DbTransaction  get; set; 
    void Commit();
    void Rollback();
    void BeginTransaction();
    void SaveChanges();

然后实现它:

public class EfContextProvider : IDbContextProvider

    public EfContextProvider(YourContext context)
    
        Context = context;
    
    public YourContext Context  set; get; 
    public DbContextTransaction DbTransaction  set; get; 

    public void Commit()
    
        DbTransaction.Commit();
    

    public void Rollback()
    
        DbTransaction.Rollback();
    

    public void BeginTransaction()
    
        DbTransaction=Context.Database.BeginTransaction();
    

    public void SaveChanges()
    
        Context.SaveChanges();
    

所以现在给你的类提供 IDbContextProvider 依赖并使用它(它里面也有上下文)。也许用 _contextProvider.BeginTransaction(); 替换 using 块;然后还有 _contextProvider.Commit();或 _contextProvider.Rollback();

【讨论】:

是否应该实现 IDisposable 以便在用于确保 DbContextTransaction 被释放时可以将其包装在 Using 语句中?【参考方案3】:

我花了几个小时试图弄清楚,我相信它可以由 MS Fakes 直接完成,无需包装器或新类。

你需要做三个步骤:

    为 DbContextTransaction 创建 shim 对象,并绕道其 Commit 和 Rollback 方法不做任何事情。 为数据库创建垫片对象。并绕道其 BeginTransaction 方法返回在步骤 1 中创建的 DbContextTransaction shim 对象。 绕过所有实例的 DbContext.Database 属性以返回在步骤 2 中创建的数据库 shim 对象。

仅此而已。

    static void SetupDBTransaction()
    
        System.Data.Entity.Fakes.ShimDbContextTransaction transaction = new System.Data.Entity.Fakes.ShimDbContextTransaction();
        transaction.Commit = () =>  ;
        transaction.Rollback = () =>  ;

        System.Data.Entity.Fakes.ShimDatabase database = new System.Data.Entity.Fakes.ShimDatabase();
        database.BeginTransactionIsolationLevel = (isolationLevel) =>return transaction.Instance;;

        System.Data.Entity.Fakes.ShimDbContext.AllInstances.DatabaseGet = (@this) =>  return database.Instance; ;
    

【讨论】:

问题到底是什么? 我最近一直在做的是在项目中创建一个 Local(db) 数据库。 问题是关于如何在实体框架中对使用事务的代码进行单元测试。可以轻松地模拟上下文类,但不能模拟事务或数据库。 @Kaboo,这很好,但我在获取 System.Data.Entity.Fakes.ShimDatabase 时遇到问题。它应该像右键单击 System.Data.Entity 引用并“添加假货”一样简单,对吗? 呵呵,右键EntityFramework引用,添加假货。【参考方案4】:

您可以将 EF 类表示为 POCO 类,并在数据库适配器类中隔离所有数据库交互。这些适配器类将具有一个您可以在测试业务逻辑时模拟的接口。

适配器类中的数据库操作可以使用真实的数据库连接进行测试,但使用专用的数据库和连接字符串进行单元测试。

那么如何测试包装在事务中的业务代码呢?

为了将业务代码与数据库适配器隔离,您必须为可以模拟的 EF 事务范围创建一个接口。

我以前使用过这样的设计,虽然不是使用 EF,但使用了类似的 POCO 包装(在伪 C# 中,未检查语法或完整性):

interface IDatabaseAdapter 

    ITransactionScope CreateTransactionScope();


interface ITransactionScope : IDisposable

    void Commit();
    void Rollback();        


class EntityFrameworkTransactionScope : ITransactionScope

    private DbContextTransaction entityTransaction;
    EntityFrameworkTransactionScope(DbContextTransaction entityTransaction)
    
        this.entityTransaction = entityTransaction;
    

    public Commit()  entityTransaction.Commit(); 
    public Rollback()  entityTransaction.Rollback(); 
    public Dispose()  entityTransaction.Dispose(); 



class EntityFrameworkAdapterBase : IDatabaseAdapter

   private Database database;
   protected EntityFrameworkAdapterBase(Database database)
   
       this.database = database;
   

   public ITransactionScope CreateTransactionScope()
   
       return new EntityFrameworkTransactionScope(database.BeginTransaction());
   


interface IIncidentDatabaseAdapter : IDatabaseAdapter

    SaveIncident(Incident incident);


public EntityIncidentDatabaseAdapter : EntityFrameworkAdapterBase, IIncidentDatabaseAdapter

    EntityIncidentDatabaseAdapter(Database database) : base(database) 

    SaveIncident(Incident incident)
    
         // code for saving the incident
    

上面的设计应该允许您为实体框架操作创建单元测试,而无需担心业务逻辑或事务,并为业务逻辑创建单元测试,您可以在其中模拟数据库故障并使用 MOQ 或类似方法来检查回滚是否真实调用您的 ITransactionScope 模拟。 有了上面的内容,您应该能够涵盖您能想到的业务逻辑中任何阶段的几乎所有事务失败。

当然,您应该用一些好的集成测试来补充您的单元测试,因为事务可能很棘手,尤其是并发使用时可能会发生棘手的死锁,而这些死锁在模拟测试中很难捕捉到。

【讨论】:

【参考方案5】:

我们使用以下代码实现了 ivaylo-pashov 的解决方案:

//Dependency Injection
public static void RegisterTypes(IUnityContainer container)
        
            // Register manager mappings.
            container.RegisterType<IDatabaseContextProvider, EntityContextProvider>(new PerResolveLifetimeManager());
        
    

//Test Setup
        /// <summary>
        ///     Mocked <see cref="IrdEntities" /> context to be used in testing.
        /// </summary>
        private Mock<CCMSEntities> _irdContextMock;
        /// <summary>
        ///     Mocked <see cref="IDatabaseContextProvider" /> context to be used in testing.
        /// </summary>
        private Mock<IDatabaseContextProvider> _EntityContextProvider;
...

            _irdContextMock = new Mock<CCMSEntities>();
            _irdContextMock.Setup(m => m.Outbreaks).Returns(new Mock<DbSet<Outbreak>>().SetupData(_outbreakData).Object);
            _irdContextMock.Setup(m => m.FDI_Number_Counter).Returns(new Mock<DbSet<FDI_Number_Counter>>().SetupData(new List<FDI_Number_Counter>()).Object);

            _EntityContextProvider = new Mock<IDatabaseContextProvider>();
            _EntityContextProvider.Setup(m => m.Context).Returns(_irdContextMock.Object);

            _irdOutbreakRepository = new IrdOutbreakRepository(_EntityContextProvider.Object, _loggerMock.Object);

// Usage in the Class being tested:
//Constructor
        public IrdOutbreakRepository(IDatabaseContextProvider entityContextProvider, ILogger logger)
        
            _entityContextProvider = entityContextProvider;
            _irdContext = entityContextProvider.Context;
            _logger = logger;
        

        /// <summary>
        ///     The wrapper for the Entity Framework context and transaction.
        /// </summary>
        private readonly IDatabaseContextProvider _entityContextProvider;

        // The usage of a transaction that automatically gets mocked because the return type is void.
        _entityContextProvider.BeginTransaction();
...

【讨论】:

【参考方案6】:

您需要的是可以调用 Commit() 和 Rollback() 并且形状类似于 System.Data.Entity.DbContextTransaction 的东西,对吧?因此,事实证明您可以在任何真实数据库上使用真实的 DbContextTransaction。然后,只要您的测试代码没有对用于事务的数据库进行任何真正的更改,Commit() 或 Rollback() 就会成功并且什么都不做。

在我的应用程序中,web api 层需要在 db 事务中执行几个业务逻辑操作,这样如果第二个操作出错,第一个操作就不会发生。我在我的业务逻辑接口中添加了一个方法来返回 Web api 层可以使用的事务。在我对该代码的测试中,我模拟了在空测试数据库上返回 DbContextTransaction 的方法。这是我使用的设置代码:

var scope = (new PConn.DataAccess.PressConnEntities()).Database.BeginTransaction();
var bizl = new Mock<IOrderMgr>();
bizl.Setup(m => m.CreateNewOrder(7, It.IsAny<string>(), It.IsAny<string>())).Returns(_testOrder1);
// .GetOrdersQuery(channel, beginUTC, endUTC);
bizl.Setup(m => m.GetOrdersQuery(7, It.IsAny<DateTime>(), It.IsAny<DateTime>())).Returns(matchedOrdersList.AsQueryable());
bizl.Setup(m => m.BeginTransaction()).Returns(scope);

只有代码 sn-p 的第一行和最后一行对于您要解决的问题很重要。

总结一下:

    更改您的代码以从您可以在测试中模拟的方法中获取事务。 当您模拟返回事务的方法时,将真实事务返回到测试数据库。 确保您的测试的 App.Config 文件具有合法配置,以便连接到测试数据库。 Commit() 和 Rollback() 随心所欲。没有数据更改,因为您已经模拟了所有 DbSet 和 DbContext 操作。

这是我使用(并非如此)虚假交易的测试代码示例:

using (var scope = this.OrderManager.BeginTransaction())

    PrintOrder pconnOrder = this.OrderManager.CreateNewOrder(channel, payload, claimsIdentity.Name);
    bool parseResult = this.OrderManager.ParseNewOrder(pconnOrder, claimsIdentity.Name, out parseErrorMessage);

    if (!parseResult)
    
        // return a fault to the caller
        HttpResponseMessage respMsg = new HttpResponseMessage(HttpStatusCode.BadRequest);
        respMsg.Content = new StringContent(parseErrorMessage);

        throw (new HttpResponseException(respMsg));
    

    scope.Commit();
    return (pconnOrder.PrintOrderID);

【讨论】:

以上是关于模拟数据库事务?的主要内容,如果未能解决你的问题,请参考以下文章

模拟Spring事务注解

ABAP 中的自治事务模拟

数据存储模拟器返回“事务内只允许祖先查询”。它不支持 Datastore 模式下的 Firestore 吗?

Spring事务管理---中

php多进程模拟并发事务产生的问题

Java操作数据库(三,趣味理解JDBC事务)