TDD for Web API with NUnit, NSubtitute

I am still confused about some TDD concepts and how to get it right. I am trying to use it for a new project using the Web API. I've read a lot about this and some article suggests NUnit as a testing framework and NSubstitute to mock the repository.

What I don't understand in NSubstitute, we can define the expected result of what we want, is it really if we want to test our code logic?

Let's say I have a controller like this with a method Put

and 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();
    }

}

      

and I am creating 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 test the logic of my controller.

Please ask for more information if there is anything that needs clarification.

+3


source to share


2 answers


If the code you actually posted is a true reflection of your test's relationship to code, then you don't seem to be taking the TDD approach. One of the main concepts is that you don't write code that hasn't been tested. This means that as a general rule of thumb, you need to have at least one test for each branch of your code, otherwise there would be no reason for the branch to be written.

Looking at your method DeleteClient

, there are three branches, so there must be at least three tests for the method (you 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 along these different paths so that you can test them. So, to redirect the InternalError branch, you have to set up your lookup element 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 interface IRepository

, it can't be 100% more accurate about setting up NSubstitute, but basically the above said when a Delete

substitution method is called with the specified parameter types (int, int, string, string, int) the replacer should return the value that is Error

set to SomeError

( this is a trigger for a branch of logic InternalError

). Then you assert that when you invoke the system under test, it returns InternalServerError

.



You need to repeat this for each of your logical branches. Don't forget that you need to set up the replacement to return all matching values ​​in order to get to each branch of logic. So, to branch ReturnsNotFound

out you will need to return the repository NoError

and the negative data value.

I said above, you needed at least one test for each branch of logic. This is the bare minimum because there are other things you want to test. In the above alternate settings, you will notice that I am using Args.Any<int>

etc. This is because for the behavior that the above tests are interested in, it doesn't really matter if the correct values ​​are passed to the repository or not, These tests check the logical flows that are affected by the return values ​​of the repository. For your testing to be complete, you also need to ensure that the correct values ​​are passed to the repository. Depending on your approach, you may have a test for one parameter, or you may have a test to test all parameters in a call to your repository.

To test all the parameters, taking the test ReturnsInternalError

as a base, you just need to add a test call to the subroutine something like this to validate the parameters:

clientsRepository.Received().Delete(accountId, userId, "", "", clientId);

      

I use the test ReturnsInternalError

as a base because after checking the call, I want to exit the method under test as quickly as possible, and in this case return it.

+1


source


First, when coding in TDD, you should make the smallest features possible. Approximately three lines of code (excluding parentheses and caption). A function should only have one purpose. Example: The GetEncriptedData function should call the other two GetData and EncryptData methods instead of receiving data and encrypting it. If your tdd is well done it shouldn't be a problem to achieve this result. When functions are too long, tests are pointless as they cannot really cover all of your logic. And my tests Use availability when then logic. Example: HaveInitialSituationA_WhenDoingB_ThenShouldBecomeC is the name of the test. You will find three blocks of code inside your test that represent these three parts. More. When you do tdd, you always take one step at a time. If you expect your function to return 2, then do a test,which will check if it will return two and make your function literally return. 2. If you want to get some conditions and test them in other test cases, and all of your tests must end at the end, TDD is a completely different way of coding. You do one test, it fails, you do the necessary code to make it pass, and you do another test, it fails ... This is my experience and my TDD implementation way tells me that you are wrong. But this is my point of view. I hope I helped you.you do the necessary code to make it pass and you do another test, it fails ... This is my experience and my TDD implementation way tells me you are wrong. But this is my point of view. I hope I helped you.you do the necessary code to make it pass and you do another test, it fails ... This is my experience and my TDD implementation way tells me you are wrong. But this is my point of view. I hope I helped you.



+1


source







All Articles