martinlabs
martinlabs

Reputation: 1032

TDD for Web API with NUnit, NSubtitute

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

Answers (2)

forsvarir
forsvarir

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

Fjodr
Fjodr

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

Related Questions