[英]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)数据库中进行集成测试而不是单元测试。
让我向您推荐四个帮助库,它们可以使您的测试更容易:
这里的先决条件是将您的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
。
在断言阶段,您可以查询数据并对数据进行断言。 由于您不调用Add
、 Remove
或任何其他 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
它的工作方式或多或少与以前的库相同。
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.