.NET Core如何进行单元测试服务?

wtf*_*512 3 c# xunit.net .net-core asp.net-core

我已经构建了一个WebAPI,并希望创建一个单元测试项目来自动测试我的服务.

我的WebAPI流程很简单:

控制器(DI服务) - >服务(DI存储库) - > _repo CRUD

假设我有这样的服务:

public int Cancel(string id) //change status filed to 'n'
{
    var item = _repo.Find(id);
    item.status = "n";
    _repo.Update(item);
    return _repo.SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)

我想构建一个单元测试,它只使用InMemoryDatabase.

public void Cancel_StatusShouldBeN() //Testing Cancel() method of a service
{
    _service.Insert(item); 

    int rs = _service.Cancel(item.Id);
    Assert.Equal(1, rs);

    item = _service.GetByid(item.Id);
    Assert.Equal("n", item.status);
}
Run Code Online (Sandbox Code Playgroud)

我搜索了其他相关问题,发现了

您不能在测试类上使用依赖注入.

我只是想知道是否有任何其他解决方案可以实现我的单元测试理念?

pok*_*oke 7

在单元测试时,您应该只提供您正在测试的类的所有依赖项.这是依赖注入; 没有服务构建它自己的依赖,但让它依赖外部组件来提供它们.如果你是一个依赖注入容器的外面,单元测试,其中里面手动创建你测试的类,它是你的责任,提供的依赖关系.

实际上,这意味着您要么为构造函数提供模拟或实际对象.例如,您可能希望提供一个真正的记录器,但没有目标,一个带有连接的内存数据库的真实数据库上下文,或一些模拟服务.

让我们假设在这个例子中,您正在测试的服务如下所示:

public class ExampleService
{
    public ExampleService(ILogger<ExampleService> logger,
        MyDbContext databaseContext,
        UtilityService utilityService)
    {
        // …
    }
    // …
}
Run Code Online (Sandbox Code Playgroud)

所以为了测试ExampleService,我们需要提供这三个对象.在这种情况下,我们将针对每个进行以下操作:

  • ILogger<ExampleService> - 我们将使用真正的记录器,没有任何附加目标.因此,对记录器的任何调用都将正常工作,而无需我们提供一些模拟,但我们不需要测试日志输出,因此我们不需要真正的目标
  • MyDbContext - 在这里,我们将使用附加的内存数据库中的真实数据库上下文
  • UtilityService - 为此,我们将创建一个mock,它只是在我们想要测试的方法中设置我们需要的实用程序方法.

所以单元测试看起来像这样:

[Fact]
public async Task TestExampleMethod()
{
    var logger = new LoggerFactory().CreateLogger<ExampleService>();
    var dbOptionsBuilder = new DbContextOptionsBuilder().UseInMemoryDatabase();

    // using Moq as the mocking library
    var utilityServiceMock = new Mock<UtilityService>();
    utilityServiceMock.Setup(u => u.GetRandomNumber()).Returns(4);

    // arrange
    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // fix up some data
        db.Set<Customer>().Add(new Customer()
        {
            Id = 2,
            Name = "Foo bar"
        });
        await db.SaveChangesAsync();
    }

    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // create the service
        var service = new ExampleService(logger, db, utilityServiceMock.Object);

        // act
        var result = service.DoSomethingWithCustomer(2);

        // assert
        Assert.NotNull(result);
        Assert.Equal(2, result.CustomerId);
        Assert.Equal("Foo bar", result.CustomerName);
        Assert.Equal(4, result.SomeRandomNumber);
    }
}
Run Code Online (Sandbox Code Playgroud)

在您的特定Cancel情况下,您希望避免使用您当前未测试的任何服务方法.因此,如果您想测试Cancel,您应该从服务中调用的唯一方法是Cancel.测试看起来像这样(只是在这里猜测依赖关系):

[Fact]
public async Task Cancel_StatusShouldBeN()
{
    var logger = new LoggerFactory().CreateLogger<ExampleService>();
    var dbOptionsBuilder = new DbContextOptionsBuilder().UseInMemoryDatabase();

    // arrange
    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // fix up some data
        db.Set<SomeItem>().Add(new SomeItem()
        {
            Id = 5,
            Status = "Not N"
        });
        await db.SaveChangesAsync();
    }

    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        // create the service
        var service = new YourService(logger, db);

        // act
        var result = service.Cancel(5);

        // assert
        Assert.Equal(1, result);
    }

    using (var db = new MyDbContext(dbOptionsBuilder.Options))
    {
        var item = db.Set<SomeItem>().Find(5);
        Assert.Equal(5, item.Id);
        Assert.Equal("n", item.Status);
    }
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句.请注意,我一直在打开一个新的数据库上下文,以避免从缓存的实体中获取结果.通过打开一个新的上下文,我可以验证这些更改是否真正完全进入了数据库.

  • @sajadre 这有点灰色地带。你是对的,这不是一个纯粹的单元测试,因为这不仅仅涉及正在测试的单元(“YourService.Cancel”方法)。但是,当您的单元与 EF 数据库上下文交互时,您正在处理 IQueryables。正确地模拟这些是非常复杂的,因此使用内存数据库使这变得更加容易。对模拟的可查询对象进行测试,否则最终只会复制您在单元中调用的确切查询(这使得这是一个糟糕的测试)。 (4认同)
  • @FabioSalvalai 如果您不使用显式存储库模式,则测试上下文被“使用正确的”参数调用确实很困难,因为您经常与例如“DbSet”对象进行交互,然后您还必须模拟这些对象。使用内存数据库(对于 EF Core来说“非常好”,并且专门用于此目的)使其确实更易于维护。但总的来说,我同意您应该尽可能少地使用“真实”对象,以避免其他实现可能出现的回归。 (2认同)