2015-05-15 3 views
3

Я все еще смущен некоторыми концепциями TDD и как это сделать правильно. Я пытаюсь использовать его для нового проекта с использованием Web API. Я много читал об этом, и в какой-то статье предлагается NUnit как платформа тестирования и NSubstitute, чтобы высмеять репозиторий.TDD для веб-API с NUnit, NSubtitute

Что я не понимаю в NSubstitute, мы можем определить ожидаемый результат того, что мы хотим, действительно ли это, если мы хотим проверить нашу логику кода?

Скажем, у меня есть контроллер, как это с 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); 
     } 
    } 

Глядя на код выше, я пишу мой блок проверяет правильно? Я чувствую, что не уверен, как он будет проверять логику моего контроллера.

Просьба получить дополнительную информацию, если есть что-то, что необходимо уточнить.

ответ

1

Если код, который вы опубликовали на самом деле, является истинным отражением отношения вашего теста к коду, то вы, похоже, не следуете подходу 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 это трудно быть 100% точным об установке NSubstitute, но в основном, выше, говорят, когда Delete метод подстановки вызывается с заданными типами параметров (int, int, string, string, int), заменитель должен возвращать значение, которое имеет Error, установленное на SomeError (это триггер для ветви логики InternalError). Затем вы утверждаете, что при вызове тестируемой системы возвращается InternalServerError.

Вам нужно повторить это для каждой из ваших логических ветвей. Не забывайте, что вам нужно настроить замену, чтобы вернуть все соответствующие значения, чтобы добраться до каждой ветви логики. Итак, чтобы добраться до ветки ReturnsNotFound, вам нужно сделать свой репозиторий обратно NoErrorи отрицательным значением данных.

Я сказал выше, вам понадобилось минимум один тест для каждой ветви логики. Это минимум, потому что есть другие вещи, которые вы хотите протестировать. В приведенных выше альтернативных настройках вы заметите, что я использую Args.Any<int> и т. Д. Это связано с тем, что для поведения, которое интересует вышеперечисленные тесты, на самом деле не имеет значения, передаются ли правильные значения в репозиторий или нет. Эти тесты проверяют логические потоки, на которые влияют возвращаемые значения репозитория. Чтобы ваше тестирование было полным, вам также необходимо убедиться, что правильные значения передаются в репозиторий. В зависимости от вашего подхода у вас может быть тест на каждый параметр, или у вас может быть тест для проверки всех параметров в вызове в ваш репозиторий.

Для проверки всех параметров, принимая тест ReturnsInternalError в качестве основы, вы бы просто добавить вызов проверки на subsistute что-то подобное для проверки параметров:

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

Я использую тест ReturnsInternalError в качестве основы, потому что после проверки вызова я хочу как можно быстрее выйти из тестируемого метода, и в этом случае он возвращает ошибку.

+0

А я вижу, у меня есть идея. Спасибо за широкое объяснение моего вопроса. –

1

Во-первых, при кодировании в TDD вы должны сделать наименьшие функции. Примерно три строки кода (исключая скобки и подпись). Функция должна иметь только одну цель. Пример: функция GetEncriptedData должна вызывать два других метода GetData и EncryptData вместо того, чтобы получать данные и шифровать их. Если ваш tdd хорошо сделан, это не должно быть проблемой для достижения этого результата. Когда функции слишком длинные, тесты бессмысленны, так как они не могут действительно охватывать всю вашу логику. И мои тесты Используйте наличие, когда тогда логика. Пример: HaveInitialSituationA_WhenDoingB_ThenShouldBecomeC - это имя теста. В вашем тесте вы найдете три блока кода, представляющих эти три части. Больше. Когда вы делаете tdd, вы всегда делаете один шаг сразу. Если вы ожидаете, что ваша функция вернет 2, сделайте тест, который будет проверять, вернет ли он два, и сделайте вашу функцию буквально возвращенной. 2. Если вы хотите получить некоторые условия и проверить их в других тестовых случаях, и все ваши тесты должны заканчиваться в конце , TDD - это совершенно другой способ кодирования. Вы делаете один тест, он терпит неудачу, вы делаете необходимый код, чтобы он проходил, и вы делаете еще один тест, он терпит неудачу ... Это мой опыт, и мой способ внедрения TDD говорит мне, что вы ошибаетесь. Но это моя точка зрения. Надеюсь, я помог тебе.

+0

Итак, вы говорите, что если у меня есть более 3 задач в моем методе, это похоже на «запах кода»? И как-то я должен его реорганизовать. –

+1

Более трех строк, включая все. даже деклараций. И true tdd не должен требовать рефакторинга, потому что он должен быть в порядке, как только он сделан из тестов. Если код неправильный, значит, сам тест не соответствует действительности. Но, как я уже сказал, tdd - это предельный и точный способ кодирования. Это не просто 100% охват. – Fjodr

+0

У вас есть ссылка или пример проекта, чтобы лучше понять TDD? –