2017-07-31 75 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