Reputation: 1032
I'm still confused with some TDD concepts and how to do it correctly. I'm trying to use it implement for a new project using Web API. I have read a lot about it, and some article suggest NUnit as a testing framework and NSubstitute to mock the repository.
What I don't understand is with NSubstitute we can define the expected result of what we want, is this valid if we want to validate our code logic?
Let's say I have a controller like this with Put
and Delete
method:
[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();
}
}
and I create some unit tests like this:
[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);
}
}
Looking at the code above, am I writing my unit tests correctly? I feel like I'm not sure how it will validate the logic in my controller.
Please ask for more information, if there is anything that needs clarified.
Upvotes: 4
Views: 2848
Reputation: 10839
If the code you've actually posted is a true reflection of your test to code ratio, then you don't appear to be following a TDD approach. One of the core concepts is that you don't write code that hasn't been tested. This means that as a basic rule, you need to have a minimum of one test for every branch in your code, otherwise there would be no reason for the branch to have been written.
Looking at your DeleteClient
method there are three branches, so there should be at least three tests for the method (you've only posted two).
// 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
You can use NSubtitute
to redirect your code down these different paths so that they can be tested. So, to redirect down the InternalError branch you would setup your substitute something like this:
clientsRepository.Delete(Args.Any<int>(), Args.Any<int>(),
Args.Any<string>(), Args.Any<string>(),
Args.Any<int>())
.Returns(new Result<int>()
{
Error = SomeError;
});
Without knowing the IRepository
interface it's hard to be 100% accurate about the NSubstitute setup, but basically, the above is saying when the Delete
method of the substitute is called with the given parameter types (int,int,string,string,int) the substitute should return a value which has Error
set to SomeError
(this is the trigger for the InternalError
branch of logic). You would then assert that when calling the system under test, it returns InternalServerError
.
You need to repeat this for each of your logic branches. Don't forget that you'll need to setup the substitute to return all appropriate values for to get to each branch of logic. So, to get to the ReturnsNotFound
branch, you'd need to make your repository return NoError
and a negative Data value.
I said above, you needed a minimum of one test for each branch of logic. It's a minimum because there are other things that you will want to test. In the above substitute setups, you'll notice that I'm using Args.Any<int>
etc. That's because for the behaviour the tests above are interested in, it doesn't really matter if the correct values are being passed to the repository or not. Those tests are testing the logic flows influenced by the return values of the repository. For your testing to be complete, you'll also need to make sure that the correct values are being passed to the repository. Depending on your approach, you might have a test per parameter, or you might have a test to validate all of the parameters in the call to your repository.
To validate all of the parameters, taking the ReturnsInternalError
test as a base, you would simply have to add a validation call to the subsistute something like this to validate the parameters:
clientsRepository.Received().Delete(accountId, userId, "", "", clientId);
I'm using the ReturnsInternalError
test as a base because after validating the the call, I want to get out of the method under test as fast as possible and in this instance it's by returning an error.
Upvotes: 3
Reputation: 923
First, when coding in TDD, you must make the smallest functions possible. About three lines of code (excluding brackets and signature) THe function should have only one purpose. Ex: a function called GetEncriptedData should call two other methods GetData and EncryptData instead of getting the data and encrypt it. If your tdd is well done, that shouldn't be a problem to get to that result. When functions are too long, tests are pointless as they can't really cover all your logic. And my tests Use the having when then logic. Ex.: HavingInitialSituationA_WhenDoingB_ThenShouldBecomeC is the name of the test. You would find three blocks of code inside your test representing these three parts. There is more. When doing tdd, you shoud always do one step at once. If you expect your function to return 2, make a test that validates if it returns two, and make your function literally return 2. Afther that you could want some conditions and test them in other test cases and all your tests should bass at the end. TDD is a completely different way to code. YOu make one test, it fails, you make the necessary code so it pass, and you make another test, it fails... This is my experience, and my way of implementing TDD tells me that you are wrong. But this is my point of view. Hope I helped you.
Upvotes: 1