2017-07-31 53 views
2

我已经构建了一个WebAPI并希望创建一个单元测试项目,以便自动测试我的服务。.NET核心如何进行单元测试服务?

我的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); 
} 

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

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

我只想知道是否有任何其他解决方案来达到我的单元测试想法?

+0

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

回答

5

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

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

假设在这个例子中,该服务正在测试这个样子的:

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

所以为了测试ExampleService,我们需要提供这三个对象。在这种情况下,我们会做以下每个:

  • ILogger<ExampleService> - 我们将用一个真实的记录,没有任何连接的目标。因此,任何对记录器的调用都能正常工作,而我们不需要提供一些模拟,但我们不需要测试日志输出,因此我们不需要实际的目标。
  • 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); 
    } 
} 

Btw。请注意,我始终打开一个新的数据库上下文,以避免从缓存的实体获取结果。通过打开一个新的上下文,我可以验证这些更改实际上是否完全将其导入到数据库中。

+0

很好的解释,但是,我个人会避免使用内存数据库。我想我只会测试使用正确的参数调用db上下文。也许确保db上下文映射到正确的表可以在与Cancel方法无关的专用单元测试中进行测试。 –

+0

@FabioSalvalai如果您不使用显式的存储库模式,那么测试使用正确的参数调用上下文非常困难,因为您经常与例如'DbSet'对象,然后你也必须模拟。使用内存数据库(这对于EF Core来说非常好,专门用于此目的)使得它更易于维护。但总的来说,我会同意尽可能少用“真实”的对象,以避免其他实现可能出现的退步。 – poke

+0

非常非常有用的解释! – wtf512

相关问题