[英]TDD for Web API with NUnit, NSubtitute
我仍然對一些TDD概念以及如何正確地做到這一點感到困惑。 我正在嘗試使用Web API實現一個新項目。 我已經閱讀了很多關於它的內容,有些文章建議使用NUnit作為測試框架,使用NSubstitute來模擬存儲庫。
我不明白的是,對於NSubstitute,我們可以定義我們想要的預期結果,如果我們想驗證我們的代碼邏輯,這是否有效?
假設我有一個像Put
這樣的控制器,使用Put
和Delete
方法:
[BasicAuthentication]
public class ClientsController : BaseController
{
// Dependency injection inputs new ClientsRepository
public ClientsController(IRepository<ContactIndex> clientRepo) : base(clientRepo) { }
[HttpPut]
public IHttpActionResult PutClient(string accountId, long clientId, [FromBody] ClientContent data, string userId = "", string deviceId = "", string deviceName = "")
{
var result = repository.UpdateItem(new CommonField()
{
AccountId = accountId,
DeviceId = deviceId,
DeviceName = deviceName,
UserId = userId
}, clientId, data);
if (result.Data == null)
{
return NotFound();
}
if (result.Data.Value != clientId)
{
return InternalServerError();
}
IResult<IDatabaseTable> updatedData = repository.GetItem(accountId, clientId);
if (updatedData.Error)
{
return InternalServerError();
}
return Ok(updatedData.Data);
}
[HttpDelete]
public IHttpActionResult DeleteClient(string accountId, long clientId, string userId = "", string deviceId = "")
{
var endResult = repository.DeleteItem(new CommonField()
{
AccountId = accountId,
DeviceId = deviceId,
DeviceName = string.Empty,
UserId = userId
}, clientId);
if (endResult.Error)
{
return InternalServerError();
}
if (endResult.Data <= 0)
{
return NotFound();
}
return Ok();
}
}
我創建了一些像這樣的單元測試:
[TestFixture]
public class ClientsControllerTest
{
private ClientsController _baseController;
private IRepository<ContactIndex> clientsRepository;
private string accountId = "account_id";
private string userId = "user_id";
private long clientId = 123;
private CommonField commonField;
[SetUp]
public void SetUp()
{
clientsRepository = Substitute.For<IRepository<ContactIndex>>();
_baseController = new ClientsController(clientsRepository);
commonField = new CommonField()
{
AccountId = accountId,
DeviceId = string.Empty,
DeviceName = string.Empty,
UserId = userId
};
}
[Test]
public void PostClient_ContactNameNotExists_ReturnBadRequest()
{
// Arrange
var data = new ClientContent
{
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
clientsRepository.CreateItem(commonField, data)
.Returns(new Result<long>
{
Message = "Bad Request"
});
// Act
var result = _baseController.PostClient(accountId, data, userId);
// Asserts
Assert.IsInstanceOf<BadRequestErrorMessageResult>(result);
}
[Test]
public void PutClient_ClientNotExists_ReturnNotFound()
{
// Arrange
var data = new ClientContent
{
contactName = "TestContactName 1",
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
clientsRepository.UpdateItem(commonField, clientId, data)
.Returns(new Result<long?>
{
Message = "Data Not Found"
});
var result = _baseController.PutClient(accountId, clientId, data, userId);
Assert.IsInstanceOf<NotFoundResult>(result);
}
[Test]
public void PutClient_UpdateSucceed_ReturnOk()
{
// Arrange
var postedData = new ClientContent
{
contactName = "TestContactName 1",
shippingName = "TestShippingName 1",
shippingAddress1 = "TestShippingAdress 1"
};
var expectedResult = new ContactIndex() { id = 123 };
clientsRepository.UpdateItem(commonField, clientId, postedData)
.Returns(new Result<long?> (123)
{
Message = "Data Not Found"
});
clientsRepository.GetItem(accountId, clientId)
.Returns(new Result<ContactIndex>
(
expectedResult
));
// Act
var result = _baseController.PutClient(accountId, clientId, postedData, userId)
.ShouldBeOfType<OkNegotiatedContentResult<ContactIndex>>();
// Assert
result.Content.ShouldBe(expectedResult);
}
[Test]
public void DeleteClient_ClientNotExists_ReturnNotFound()
{
clientsRepository.Delete(accountId, userId, "", "", clientId)
.Returns(new Result<int>()
{
Message = ""
});
var result = _baseController.DeleteClient(accountId, clientId, userId);
Assert.IsInstanceOf<NotFoundResult>(result);
}
[Test]
public void DeleteClient_DeleteSucceed_ReturnOk()
{
clientsRepository.Delete(accountId, userId, "", "", clientId)
.Returns(new Result<int>(123)
{
Message = ""
});
var result = _baseController.DeleteClient(accountId, clientId, userId);
Assert.IsInstanceOf<OkResult>(result);
}
}
看看上面的代碼,我是否正確編寫了單元測試? 我覺得我不確定它將如何驗證控制器中的邏輯。
如果有任何需要澄清的話,請詢問更多信息。
如果您實際發布的代碼真實地反映了您的測試代碼比率,那么您似乎沒有遵循TDD方法。 其中一個核心概念是您不編寫尚未經過測試的代碼。 這意味着作為一個基本規則,您需要為代碼中的每個分支至少進行一次測試,否則就沒有理由編寫分支。
查看您的DeleteClient
方法有三個分支,因此該方法應該至少有三個測試(您只發布了兩個)。
// Test1 - If repo returns error, ensure expected return value
DeleteClient_Error_ReturnsInternalError
// Test2 - If repo returns negative data value, ensure expected return value
DeleteClient_NoData_ReturnsNotFound
// Test3 - If repo returns no error, ensure expected return
DeleteClient_Success_ReturnsOk
您可以使用NSubtitute
將代碼重定向到這些不同的路徑,以便可以對它們進行測試。 因此,要重定向InternalError分支,您可以設置這樣的替換:
clientsRepository.Delete(Args.Any<int>(), Args.Any<int>(),
Args.Any<string>(), Args.Any<string>(),
Args.Any<int>())
.Returns(new Result<int>()
{
Error = SomeError;
});
在不知道IRepository
接口的情況下,很難對NSubstitute設置100%准確,但基本上,上面說的是當使用給定的參數類型(int,int,string,string,int)調用替換的Delete
方法時substitute應該返回一個將Error
設置為SomeError
的值(這是邏輯的InternalError
分支的觸發器)。 然后,您將斷言在調用受測試的系統時,它會返回InternalServerError
。
您需要為每個邏輯分支重復此操作。 不要忘記您需要設置替換以返回所有適當的值以獲取邏輯的每個分支。 因此,要訪問ReturnsNotFound
分支,您需要使存儲庫返回NoError
和負數據值。
我上面說過,每個邏輯分支至少需要一個測試。 這是最低限度的,因為您還需要測試其他內容。 在上面的替換設置中,你會注意到我正在使用Args.Any<int>
等。這是因為對於上面測試感興趣的行為,如果將正確的值傳遞給存儲庫與否。 這些測試正在測試受存儲庫返回值影響的邏輯流程。 為了使測試完整,您還需要確保將正確的值傳遞給存儲庫。 根據您的方法,您可能會對每個參數進行測試,或者您可能需要進行測試以驗證對存儲庫的調用中的所有參數。
要驗證所有參數,以ReturnsInternalError
測試為基礎,您只需要向subsistute添加一個驗證調用,以驗證參數:
clientsRepository.Received().Delete(accountId, userId, "", "", clientId);
我使用ReturnsInternalError
測試作為基礎,因為在驗證調用之后,我想盡可能快地退出測試中的方法,在這種情況下,它是通過返回錯誤。
首先,在TDD編碼時,必須使最小的功能成為可能。 大約三行代碼(不包括括號和簽名)該函數應該只有一個目的。 例如:一個名為GetEncriptedData的函數應該調用另外兩個方法GetData和EncryptData,而不是獲取數據並對其進行加密。 如果您的tdd做得好,那么獲得該結果應該不是問題。 當函數太長時,測試毫無意義,因為它們無法覆蓋所有邏輯。 我的測試使用當時的邏輯。 例如:HavingInitialSituationA_WhenDoingB_ThenShouldBecomeC是測試的名稱。 您將在測試中找到代表這三個部分的三個代碼塊。 還有更多。 在做tdd時,你總是要一步一步。 如果你希望你的函數返回2,那么做一個測試驗證它是否返回2,並使你的函數字面上返回2.如果你可能需要一些條件並在其他測試用例中測試它們而你所有的測試都應該低音到底。 TDD是一種完全不同的代碼方式。 你做了一個測試,它失敗了,你做了必要的代碼,所以它通過了,你做了另一個測試,它失敗了...這是我的經驗,我實施TDD的方式告訴我你錯了。 但這是我的觀點。 希望我能幫助你。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.