繁体   English   中英

单元测试包含 Using 块的方法

[英]Unit Testing a method which contains a Using block

我已经写了(几年前写的)C# function,我被要求用单元测试来覆盖这个方法。

public string PlaceOrder(int requestId, string orderedby)
    {
        try
        {
            using (DatabaseContext dbContext = new DatabaseContext("myConnectionStringHere"))
            {
                var req = dbContext.Orders.Where(row => row.id == requestId).FirstOrDefault();
                if (req == null)
                    return "not found";
               
                req.status="A";
                dbContext.SaveChanges();
                return "found";
            }
        }
        catch (Exception ex)
        {
            return "error";
        }
    }

现在,在进行单元测试时,我需要确保它不会向数据库写入任何内容,因此我必须对其进行MOQ 我怎么能MOQ ,它包含Using块。

我知道架构本来可以更好,应该遵循设计模式,但我不允许更改应用程序的结构,因为它是遗留应用程序。

这里的一般指导是更喜欢在 memory(w/o sqlite)数据库中进行集成测试而不是单元测试。

让我向您推荐四个帮助库,它们可以使您的测试更容易:

EntityFrameworkCoreMock

Github 链接

这里的先决条件是将您的DbSet标记为virtual的,如下所示:

public virtual DbSet<Order> Orders { get; set; }

然后你可以创建一个模拟,你可以用一些虚拟数据填充你的Orders集合:

var initialOrders = new[]
{
    new Order { ... },
    new Order { ... },
};

var dbContextMock = new DbContextMock<DatabaseContext>(new DbContextOptionsBuilder<DatabaseContext>().Options);
var ordersDbSetMock = dbContextMock.CreateDbSetMock(db => db.Orders, initialOrders);

您必须重写包含 class 的PlaceOrder方法,以便在构造函数中接收DatabaseContext参数,以便能够在测试期间注入dbContextMock.Object

在断言阶段,您可以查询数据并对数据进行断言。 由于您不调用AddRemove或任何其他 CRUD 方法,因此您只能Verify SaveChanges调用。

public void GivenAnExistingOrder_WhenICallPlaceOrder_ThenSaveChangesIsCalledOnce()
{
   ...
   //Assert
   dbMock.Verify(db => db.SaveChanges(), Times.Once);
}

public void GivenANonExistingOrder_WhenICallPlaceOrder_ThenSaveChangesIsCalledNever()
{
   ...
   //Assert
   dbMock.Verify(db => db.SaveChanges(), Times.Never);
}

EntityFrameworkCore.Testing

Github 链接

它的工作方式或多或少与以前的库相同。

var dbContextMock = Create.MockedDbContextFor<DatabaseContext>();
dbContextMock.Set<Order>().AddRange(initialOrders);
dbContextMock.SaveChanges();

断言以相同的方式工作。


第三个(不太成熟的)库称为Moq.EntityFrameworkCore


如果您真的热衷于通过避免在 memory 数据库中执行单元测试,那么您应该尝试使用MockQueryable库。

const int requestId = 1;
var orders = new List<Order>();
var ordersMock = orders.AsQueryable().BuildMockDbSet();
ordersMock.Setup(table => table.Where(row => row.Id == requestId)).Returns(...)

在这里,您基本上是 mocking 应该是Where过滤器的结果。 为了能够使用它,包含 class 的PlaceOrder应该通过其构造函数接收DbSet<Order>参数。

或者,如果您有一个IDatabaseContext接口,那么您也可以像这样使用它:

Mock<IQueryable<Order>> ordersMock = orders.AsQueryable().Build();
Mock<IDatabaseContext> dbContextMock = ...
dbContextMock.Setup(m => m.ReadSet<Order>()).Returns(ordersMock.Object));

这里应该改变很多东西:

1:

不要直接在代码库中以这种方式实现连接字符串。 相反,将您的数据库直接注入到您的类中。

所以这个伪代码应该有助于理解一般的想法。

public void ConfigureService(IServiceCollection serviceCollection)
{
   ...

  
   string connectionString = //secure storage;
   serviceCollection.AddDbContext<DatabaseContext>(options => {

      options.UseSqlServer(connectionString);

   });

   ...
}

然后

public class OrderRepository
{
        
   private IServiceScopeFactory _serviceScopeFactory ;
        
   public OrderRepository(IServiceScopeFactory serviceScopeFactory ){
      _serviceScopeFactory = serviceScopeFactory ;
   }
   
   ...
        
        
   public string PlaceOrder(int requestId, string orderedby)
   {
      try
      {
         using (var context = serviceScopeFactory.CreateScope())
         {
            var req = context.Orders.Where(row => row.id == requestId).FirstOrDefault();
            if (req == null)
               return "not found";
                       
            req.status="A";
            context.SaveChanges();
            return "found";
         }
      }
      catch (Exception ex)
      {
         return "error";
      }
   }
    
   ...

}

如果您想进行集成测试,则可以使用 InMemory 数据库来模拟您想要的任何东西。 或者你可以连接到一个“真正的”数据库,然后这样做。

如果你想让它成为一个单元测试,你可以看到这个链接: How to setup a DbContext Mock

2:

为正在下的订单返回一个字符串,表示找到/未找到,这似乎非常适得其反。

如果您的目标是记录此信息,请提供一个可以记录此信息的 DI 记录器。 (尝试导入 ILogger 接口,它是微软对日志记录的扩展,记不住 nuget package 名称)应该可以使您非常有效地使用 DI 进行日志记录。

如果您的目标是让可能的 UI 显示此消息,则消息内容不可能源自后端或域逻辑。

至少不是这样的。

然后你应该为响应创建一个接口,并返回所述接口的实现,该接口至少存在于其他地方,但即使这样也有点像尿裤子。 (并且包含一个 UI 友好的消息,可以包含一个可能的堆栈跟踪/异常),以及其他可能的相关信息,比如您尝试下订单的 ID 等。)

你应该让它发生在你的 UI 和域逻辑之间的接口上,前提是字符串的目的是什么。 您希望看到错误处理的地方。

3:WTF 赶上了? 你只是返回错误? 出色地? 什么错误? 你以这种方式丢失了堆栈跟踪? 有人应该为此受到惩罚。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM