【问题标题】:.NET Core how to unit test service?.NET Core 如何对服务进行单元测试?
【发布时间】:2018-01-06 15:44:40
【问题描述】:

我已经构建了一个 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();
}

我想构建一个单元测试,它只使用 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);
}

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

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

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

【问题讨论】:

  • 你不应该在这种测试中使用“单元测试”这个词。这是一个集成测试,因为您的测试依赖于具体的DbContextimplementation。可以在没有外部依赖项的情况下测试单元测试(即通过模拟接口)。这是您在服务中使用 EF Core 而不将其抽象到存储库或通过 CQRS 时的缺点之一

标签: c# asp.net-core .net-core xunit.net


【解决方案1】:

在进行单元测试时,您应该明确提供您正在测试的类的所有依赖项。 依赖注入;不是让服务自己构建它的依赖关系,而是让它依赖外部组件来提供它们。当您在依赖注入容器之外并且在手动创建您正在测试的类的单元测试中时,提供依赖关系是您的责任。

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

让我们假设这个例子,你正在测试的服务看起来像这样:

public class ExampleService
{
    public ExampleService(ILogger<ExampleService> logger,
        MyDbContext databaseContext,
        UtilityService utilityService)
    {
        // …
    }
    // …
}

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

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

所以单元测试可能如下所示:

[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);
    }
}

在您特定的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);
    }
}

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

【讨论】:

  • 很好的解释,但是,我个人会避免使用内存数据库。我想我只会测试是否使用正确的参数调用了 db 上下文。也许确保数据库上下文映射到正确的表可以在与 Cancel 方法无关的专用单元测试上进行测试。
  • @FabioSalvalai 如果您不使用明确的存储库模式,那么测试上下文是否被“正确”调用是非常困难的,因为您经常与例如DbSet 然后您还必须模拟的对象。使用内存数据库(这对于 EF Core 来说非常好,并且专门用于此目的)使它真正更易于维护。但总的来说,我同意你应该尽可能少“真实”的对象,以避免其他实现可能的回归。
  • 非常有用的解释!
  • 编写这样的测试方法进行测试通常是否正确?单元测试是软件测试的一个级别,其中测试软件的单个单元/组件,我认为从技术上讲,测试是不正确的像这样作为单元测试连接到数据库的方法
  • @sajadre 这有点像灰色地带。你是对的,它不是一个纯粹的单元测试,因为它不仅仅涉及被测试的单元(YourService.Cancel 方法)。但是,当您的单元与 EF 数据库上下文交互时,您正在处理 IQueryables。并且正确地模拟这些只是超级复杂,因此使用内存数据库会使这变得容易得多。对模拟的可查询对象进行测试,否则最终只会复制您在单元中调用的确切查询(使这是一个糟糕的测试)。
猜你喜欢
  • 1970-01-01
  • 2018-05-08
  • 2022-10-25
  • 2010-09-07
  • 1970-01-01
  • 2020-07-10
  • 2014-05-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多