简体   繁体   English

单元测试包含 Using 块的方法

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

I have a already written (was written years ago) C# function, I have been asked to cover this method with Unit Tests.我已经写了(几年前写的)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";
        }
    }

Now while Unit testing I need to make sure that it does not write anything to database, so I have to MOQ it.现在,在进行单元测试时,我需要确保它不会向数据库写入任何内容,因此我必须对其进行MOQ How can I MOQ , it contains Using block.我怎么能MOQ ,它包含Using块。

I know architecture could have been better and design patterns should have been followed but I am not allowed to change the structure of the application as it is a legacy application.我知道架构本来可以更好,应该遵循设计模式,但我不允许更改应用程序的结构,因为它是遗留应用程序。

The general guidance here is to prefer Integration Test with in memory (w/o sqlite) database over unit testing.这里的一般指导是更喜欢在 memory(w/o sqlite)数据库中进行集成测试而不是单元测试。

Let me suggest you four helper libraries which can make your testing easier:让我向您推荐四个帮助库,它们可以使您的测试更容易:

EntityFrameworkCoreMock EntityFrameworkCoreMock

Github link Github 链接

The prerequisite here is to mark your DbSet as virtual like this:这里的先决条件是将您的DbSet标记为virtual的,如下所示:

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

Then you can create a mock where you can populate your Orders collection with some dummy data:然后你可以创建一个模拟,你可以用一些虚拟数据填充你的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);

You have to rewrite your containing class of the PlaceOrder method in a way to receive a DatabaseContext parameter in the constructor to be able inject dbContextMock.Object during testing.您必须重写包含 class 的PlaceOrder方法,以便在构造函数中接收DatabaseContext参数,以便能够在测试期间注入dbContextMock.Object

In the assertion phase you can query your data and make assertion against it.在断言阶段,您可以查询数据并对数据进行断言。 Since you do not call Add , Remove or any other CRUD method, you can only Verify the SaveChanges call.由于您不调用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 link Github 链接

It is working more or less in the way as the previous library.它的工作方式或多或少与以前的库相同。

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

The assertions work in the same way.断言以相同的方式工作。


A 3rd (less mature) library is called Moq.EntityFrameworkCore .第三个(不太成熟的)库称为Moq.EntityFrameworkCore


If you really keen to perform unit testing by avoiding in memory database then you should give a try to the MockQueryable library.如果您真的热衷于通过避免在 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(...)

Here you are basically mocking what should be the result of your Where filter.在这里,您基本上是 mocking 应该是Where过滤器的结果。 In order to be able to use this the containing class of the PlaceOrder should receive a DbSet<Order> parameter via its constructor.为了能够使用它,包含 class 的PlaceOrder应该通过其构造函数接收DbSet<Order>参数。

Or if you have an IDatabaseContext interface then you can use that one as well like this:或者,如果您有一个IDatabaseContext接口,那么您也可以像这样使用它:

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

Many things should be changed here:这里应该改变很多东西:

1: 1:

Do not implement your connection string this way, directly in the code base.不要直接在代码库中以这种方式实现连接字符串。 Instead, DI your database into your classes.相反,将您的数据库直接注入到您的类中。

so this pseudo code should help out with the general idea.所以这个伪代码应该有助于理解一般的想法。

public void ConfigureService(IServiceCollection serviceCollection)
{
   ...

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

      options.UseSqlServer(connectionString);

   });

   ...
}

And then然后

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";
      }
   }
    
   ...

}

if you want to make an integration test, you can then use an InMemory db to emulate whatever you want.如果您想进行集成测试,则可以使用 InMemory 数据库来模拟您想要的任何东西。 Or you can connect to a "real" db, and do it that way.或者你可以连接到一个“真正的”数据库,然后这样做。

If you want to make it a unit test, you can see at this link: How to setup a DbContext Mock如果你想让它成为一个单元测试,你可以看到这个链接: How to setup a DbContext Mock

2: 2:

returning a string saying found/not found for a order being placed, seems extremely counter productive.为正在下的订单返回一个字符串,表示找到/未找到,这似乎非常适得其反。

if your aim is to log this information, provider a DI logger, that can log this.如果您的目标是记录此信息,请提供一个可以记录此信息的 DI 记录器。 (Try importing the ILogger interface, it's a microsoft extension on logging, can't remember the nuget package name) should enable you to log with DI very efficiently. (尝试导入 ILogger 接口,它是微软对日志记录的扩展,记不住 nuget package 名称)应该可以使您非常有效地使用 DI 进行日志记录。

If your aim is to let a possible UI display this message, there is no way the message content should originate from back-end or domain logic.如果您的目标是让可能的 UI 显示此消息,则消息内容不可能源自后端或域逻辑。

At least not like this.至少不是这样的。

Then you should make an interface for a response, and return an implementation of said interface, that exists somewhere else as a minimum but even that is a bit like peeing your pants.然后你应该为响应创建一个接口,并返回所述接口的实现,该接口至少存在于其他地方,但即使这样也有点像尿裤子。 (And contains a UI friendly message, can contain a possible stacktrace/exception), and other possible relevant information, like what Id you were trying to place an order on etc.) (并且包含一个 UI 友好的消息,可以包含一个可能的堆栈跟踪/异常),以及其他可能的相关信息,比如您尝试下订单的 ID 等。)

You should make it something that happens at the interface between your UI and domain logic, provided that is what the string is intended for.你应该让它发生在你的 UI 和域逻辑之间的接口上,前提是字符串的目的是什么。 Where you would expect to see error handling.您希望看到错误处理的地方。

3: WTF is up with the catch? 3:WTF 赶上了? you just return error?你只是返回错误? Well?出色地? What error?什么错误? you lose the stack-trace this way?你以这种方式丢失了堆栈跟踪? Someone should be punished for that.有人应该为此受到惩罚。

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

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