简体   繁体   English

使用NUnit,NSubtitute的Web API的TDD

[英]TDD for Web API with NUnit, NSubtitute

I'm still confused with some TDD concepts and how to do it correctly. 我仍然对一些TDD概念以及如何正确地做到这一点感到困惑。 I'm trying to use it implement for a new project using Web API. 我正在尝试使用Web API实现一个新项目。 I have read a lot about it, and some article suggest NUnit as a testing framework and NSubstitute to mock the repository. 我已经阅读了很多关于它的内容,有些文章建议使用NUnit作为测试框架,使用NSubstitute来模拟存储库。

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? 我不明白的是,对于NSubstitute,我们可以定义我们想要的预期结果,如果我们想验证我们的代码逻辑,这是否有效?

Let's say I have a controller like this with Put and Delete method: 假设我有一个像Put这样的控制器,使用PutDelete方法:

[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. 如果有任何需要澄清的话,请询问更多信息。

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. 如果您实际发布的代码真实地反映了您的测试代码比率,那么您似乎没有遵循TDD方法。 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). 查看您的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

You can use NSubtitute to redirect your code down these different paths so that they can be tested. 您可以使用NSubtitute将代码重定向到这些不同的路径,以便可以对它们进行测试。 So, to redirect down the InternalError branch you would setup your substitute something like this: 因此,要重定向InternalError分支,您可以设置这样的替换:

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). 在不知道IRepository接口的情况下,很难对NSubstitute设置100%准确,但基本上,上面说的是当使用给定的参数类型(int,int,string,string,int)调用替换的Delete方法时substitute应该返回一个将Error设置为SomeError的值(这是逻辑的InternalError分支的触发器)。 You would then assert that when calling the system under test, it returns InternalServerError . 然后,您将断言在调用受测试的系统时,它会返回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. 因此,要访问ReturnsNotFound分支,您需要使存储库返回NoError 负数据值。

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. 在上面的替换设置中,你会注意到我正在使用Args.Any<int>等。这是因为对于上面测试感兴趣的行为,如果将正确的值传递给存储库与否。 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: 要验证所有参数,以ReturnsInternalError测试为基础,您只需要向subsistute添加一个验证调用,以验证参数:

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. 我使用ReturnsInternalError测试作为基础,因为在验证调用之后,我想尽可能快地退出测试中的方法,在这种情况下,它是通过返回错误。

First, when coding in TDD, you must make the smallest functions possible. 首先,在TDD编码时,必须使最小的功能成为可能。 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. 例如:一个名为GetEncriptedData的函数应该调用另外两个方法GetData和EncryptData,而不是获取数据并对其进行加密。 If your tdd is well done, that shouldn't be a problem to get to that result. 如果您的tdd做得好,那么获得该结果应该不是问题。 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. 例如:HavingInitialSituationA_WhenDoingB_ThenShouldBecomeC是测试的名称。 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. 在做tdd时,你总是要一步一步。 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. 如果你希望你的函数返回2,那么做一个测试验证它是否返回2,并使你的函数字面上返回2.如果你可能需要一些条件并在其他测试用例中测试它们而你所有的测试都应该低音到底。 TDD is a completely different way to code. TDD是一种完全不同的代码方式。 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. 你做了一个测试,它失败了,你做了必要的代码,所以它通过了,你做了另一个测试,它失败了...这是我的经验,我实施TDD的方式告诉我你错了。 But this is my point of view. 但这是我的观点。 Hope I helped you. 希望我能帮助你。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM