Подтвердить что ты не робот

Единичное тестирование, насмешливый - простой случай: Сервис - Репозиторий

Рассмотрим следующий кусок обслуживания:

public class ProductService : IProductService {

   private IProductRepository _productRepository;

   // Some initlization stuff

   public Product GetProduct(int id) {
      try {
         return _productRepository.GetProduct(id);
      } catch (Exception e) {
         // log, wrap then throw
      }
   }
}

Рассмотрим простой unit test:

[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
   var product = EntityGenerator.Product();

   _productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);

   Product returnedProduct = _productService.GetProduct(product.Id);

   Assert.AreEqual(product, returnedProduct);

   _productRepositoryMock.VerifyAll();
}

Сначала кажется, что этот тест в порядке. Но немного измените наш метод обслуживания:

public Product GetProduct(int id) {
   try {
      var product = _productRepository.GetProduct(id);

      product.Owner = "totallyDifferentOwner";

      return product;
   } catch (Exception e) {
      // log, wrap then throw
   }
}

Как переписать заданный тест, который он передал бы с первым методом службы, и не со вторым?

Как вы справляетесь с такими сценариями простой?

СОВЕТ 1: Данный тест является плохим продуктом coz и возвращается. Продукт действительно является тем же самым ссылкой.

СОВЕТ 2: Реализация элементов равенства (object.equals) не является решением.

СОВЕТ 3: На данный момент я создаю клон экземпляра Product (expectedProduct) с помощью AutoMapper, но мне это не нравится.

СОВЕТ 4: Я не тестирую, что SUT НЕ делает. Я пытаюсь проверить, что SUT DOES возвращает тот же объект, что и в репозитории.

4b9b3361

Ответ 1

Лично мне было бы безразлично. Тест должен быть уверен, что код делает то, что вы намереваетесь. Очень сложно проверить, что код не делает, я бы не стал беспокоиться в этом случае.

Тест на самом деле должен выглядеть следующим образом:

[Test]
public void GetProduct_GetsProductFromRepository() 
{
   var product = EntityGenerator.Product();

   _productRepositoryMock
     .Setup(pr => pr.GetProduct(product.Id))
     .Returns(product);

   Product returnedProduct = _productService.GetProduct(product.Id);

   Assert.AreSame(product, returnedProduct);
}

Я имею в виду, это одна строка кода, которую вы тестируете.

Ответ 2

Один из способов мышления модульных тестов - это кодированные спецификации. Когда вы используете EntityGenerator для создания экземпляров как для теста, так и для фактического обслуживания, ваш тест можно увидеть для выражения требования

  • Служба использует EntityGenerator для создания экземпляров продукта.

Это то, что ваш тест проверяет. Он не указан, потому что он не упоминает, разрешены ли изменения или нет. Если мы скажем

  • Служба использует EntityGenerator для создания экземпляров продукта, которые не могут быть изменены.

Затем мы получим подсказку относительно изменений теста, необходимых для захвата ошибки:

var product = EntityGenerator.Product();
// [ Change ] 
var originalOwner = product.Owner;  
// assuming owner is an immutable value object, like String
// [...] - record other properties as well.

Product returnedProduct = _productService.GetProduct(product.Id);

Assert.AreEqual(product, returnedProduct);

// [ Change ] verify the product is equivalent to the original spec
Assert.AreEqual(originalOwner, returnedProduct.Owner);
// [...] - test other properties as well

(Изменение заключается в том, что мы возвращаем владельца из только что созданного Продукта и проверяем владельца из Продукта, возвращаемого службой.)

Это отражает тот факт, что Владелец и другие свойства продукта должны быть равны исходному значению от генератора. Это может показаться, что я заявляю очевидное, так как код довольно тривиальный, но он работает довольно глубоко, если вы думаете с точки зрения спецификаций требований.

Я часто "тестирую свои тесты", оговаривая "если я изменяю эту строку кода, настраиваю критическую константу или два или вношу несколько отскоков кода (например, меняя!= на ==), какой тест будет захватывать ошибку?" Выполнение этого для реальных находок, если есть тест, который фиксирует проблему. Иногда нет, и в этом случае пришло время посмотреть на требования, неявные в тестах, и посмотреть, как их можно затянуть. В проектах, где нет реальных требований к захвату/анализу, это может быть полезным инструментом для ужесточения тестов, чтобы они не срабатывали при возникновении неожиданных изменений.

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

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

РЕДАКТИРОВАТЬ: Я отвечаю на это из-за противоречий, заданных в вопросе. Учитывая свободный выбор, я бы предложил не использовать EntityGenerator для создания экземпляров тестовых продуктов, а вместо этого создавать их "вручную" и использовать сравнение равенства. Или более прямым, сравните поля возвращаемого продукта с конкретными (жестко закодированными) значениями в тесте, опять же, без использования EntityGenerator в тесте.

Ответ 3

Почему вы не издеваетесь над product, а также productRepository?

Если вы издеваетесь над product, используя строгий макет, вы получите отказ, когда репозиторий коснется вашего продукта.

Если это совершенно смешная идея, не могли бы вы объяснить, почему? Честно говоря, я хотел бы узнать.

Ответ 4

Uhhhhhhhhhhh...................

Q1: Не вносите изменений в код, а затем пишите тест. Сначала напишите тест на ожидаемое поведение. Затем вы можете делать все, что хотите, SUT.

Q2: Вы не вносите изменения в свой Product Gateway, чтобы изменить владельца продукта. Вы вносите изменения в свою модель.

Но если вы настаиваете, слушайте ваши тесты. Они говорят вам, что у вас есть возможность вытащить продукты из шлюза, у которого есть неправильные владельцы. Ой, Похоже на бизнес-правило. Должен быть протестирован в модели.

Также вы используете макет. Почему вы проверяете детали реализации? Шлюз заботится только о том, что _productRepository.GetProduct(id) возвращает продукт. Не то, что продукт.

Если вы проверите таким образом, вы будете создавать хрупкие тесты. Что делать, если продукт меняется дальше. Теперь у вас есть тесты без проблем.

Ваши потребители продукта (MODEL) являются единственными, кто заботится о реализации Product.

Итак, ваш тест шлюза должен выглядеть так:

[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
   var product = EntityGenerator.Product();

   _productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);

   _productService.GetProduct(product.Id);

   _productRepositoryMock.VerifyAll();
}

Не помещайте бизнес-логику там, где она не принадлежит! И это следствие не проверяет бизнес-логику, где ее не должно быть.

Ответ 5

Если вы действительно хотите гарантировать, что метод службы не изменяет атрибуты ваших продуктов, у вас есть два варианта:

  • Определите ожидаемые атрибуты продукта в своем тесте и убедитесь, что полученный продукт соответствует этим значениям. (Кажется, это то, что вы делаете сейчас, клонируя объект.)

  • Извлеките продукт и определите ожидания, чтобы убедиться, что метод службы не изменяет его атрибуты.

Вот как я сделал бы последнее с NMock:

// If you're not a purist, go ahead and verify all the attributes in a single
// test - Get_Product_Does_Not_Modify_The_Product_Returned_By_The_Repository
[Test]
public Get_Product_Does_Not_Modify_Owner() {

    Product mockProduct = mockery.NewMock<Product>(MockStyle.Transparent);

    Stub.On(_productRepositoryMock)
        .Method("GetProduct")
        .Will(Return.Value(mockProduct);

    Expect.Never
          .On(mockProduct)
          .SetProperty("Owner");

    _productService.GetProduct(0);

    mockery.VerifyAllExpectationsHaveBeenMet();
}

Ответ 6

Мой предыдущий ответ стоит, хотя предполагается, что члены класса Product, о которых вы заботитесь, являются общедоступными и виртуальными. Это маловероятно, если класс является POCO/DTO.

То, что вы ищете, может быть перефразировано как способ сравнения значений (а не экземпляра) объекта.

Один из способов сравнить, если они совпадают при сериализации. Я сделал это недавно для некоторого кода... Заменял длинный список параметров параметризованным объектом. Код жесток, я не хочу реорганизовывать его, хотя его скоро уйдут. Поэтому я просто делаю это сравнение сериализации как быстрый способ увидеть, имеют ли они одинаковое значение.

Я написал некоторые служебные функции... Assert2.IsSameValue(ожидаемый, актуальный), который функционирует как NUnit Assert.AreEqual(), за исключением того, что он сериализуется через JSON перед сравнением. Аналогично, It2.IsSameSerialized() может использоваться для описания параметров, переданных для издевающихся вызовов способом, подобным Moq.It.Is().

public class Assert2
{
    public static void IsSameValue(object expectedValue, object actualValue) {

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        var expectedJSON = serializer.Serialize(expectedValue);
        var actualJSON = serializer.Serialize(actualValue);

        Assert.AreEqual(expectedJSON, actualJSON);
    }
}

public static class It2
{
    public static T IsSameSerialized<T>(T expectedRecord) {

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        string expectedJSON = serializer.Serialize(expectedRecord);

        return Match<T>.Create(delegate(T actual) {

            string actualJSON = serializer.Serialize(actual);

            return expectedJSON == actualJSON;
        });
    }
}

Ответ 7

Ну, один из способов - обмануть продукт, а не фактический продукт. Не пытайтесь повлиять на продукт, делая его строгим. (Я предполагаю, что вы используете Moq, похоже, что вы есть)

[Test]
public void GetProduct_return_the_same_product_as_getProduct_on_productRepository() {
   var product = new Mock<EntityGenerator.Product>(MockBehavior.Strict);

   _productRepositoryMock.Setup(pr => pr.GetProduct(product.Id)).Returns(product);

   Product returnedProduct = _productService.GetProduct(product.Id);

   Assert.AreEqual(product, returnedProduct);

   _productRepositoryMock.VerifyAll();
   product.VerifyAll();
}

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

Ответ 8

Я не уверен, если unit test должен заботиться о том, что данный метод делает не. Возможны миллионы шагов. В строгом тесте "GetProduct (id) возвращает тот же продукт, что и getProduct (id) на productRepository" корректно с линией product.Owner = "totallyDifferentOwner" или без нее.

Однако вы можете создать тест (если это необходимо) "GetProduct (id) вернуть продукт с тем же содержимым, что и getProduct (id) на productRepository", где вы можете создать (возможно расширенный) клон одного экземпляра продукта, а затем вы должны сравните содержимое двух объектов (поэтому нет объекта .Equals или object.ReferenceEquals).

Модульные тесты не гарантируют 100% -ную ошибку и правильное поведение.

Ответ 9

Вы можете вернуть интерфейс к продукту вместо конкретного продукта.

Например,

public IProduct GetProduct(int id) 
{ 
   return _productRepository.GetProduct(id);
}

И затем убедитесь, что свойство Owner не установлено:

Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg.Is.Anything);

Если вы заботитесь обо всех свойствах и/или методах, то, возможно, существует уже существующий способ с Rhino. В противном случае вы можете создать метод расширения, который, вероятно, использует отражение, например:

Dep<IProduct>().AssertNoPropertyOrMethodWasCalled()

Наши характеристики поведения таковы:

[Specification]
public class When_product_service_has_get_product_called_with_any_id 
       : ProductServiceSpecification
{
   private int _productId;

   private IProduct _actualProduct;

   [It] 
   public void Should_return_the_expected_product()
   {
     this._actualProduct.Should().Be.EqualTo(Dep<IProduct>());
   }

   [It]
   public void Should_not_have_the_product_modified()
   {
     Dep<IProduct>().AssertWasNotCalled(p => p.Owner = Arg<string>.Is.Anything);

     // or write your own extension method:
     // Dep<IProduct>().AssertNoPropertyOrMethodWasCalled();
   }


   public override void GivenThat()
   {
     var randomGenerator = new RandomGenerator();
     this._productId = randomGenerator.Generate<int>();

     Stub<IProductRepository, IProduct>(r => r.GetProduct(this._productId));
   }

   public override void WhenIRun()
   {
       this._actualProduct = Sut.GetProduct(this._productId);
   }
}

Enjoy.

Ответ 10

Если все потребители ProductService.GetProduct() ожидают такого же результата, как если бы они попросили его у ProductRepository, почему бы им просто не вызвать ProductRepository.GetProduct()? Кажется, здесь у вас есть нежелательный Средний человек.

В ProductService.GetProduct() добавлено не так много добавленной стоимости. Сбросьте его и попросите клиентские объекты напрямую вызвать ProductRepository.GetProduct(). Поместите обработку ошибок и войдите в ProductRepository.GetProduct() или код пользователя (возможно, через AOP).

Нет больше среднего человека, больше нет проблем с расхождением, больше нет необходимости проверять это несоответствие.

Ответ 11

Позвольте мне изложить проблему, как я ее вижу.

  • У вас есть метод и метод тестирования. Метод проверки подтверждает исходный метод.
  • Вы меняете тестируемую систему, изменяя данные. То, что вы хотите увидеть, это то, что тот же самый unit test терпит неудачу.

Таким образом, вы создаете тест, который проверяет, что данные в источнике данных соответствуют данным в выбранном объекте ПОСЛЕ того, как служебный уровень возвращает его. Вероятно, это относится к классу "теста интеграции".

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

Я думаю, что реальный вопрос: зачем тестировать свою модель обслуживания для правильности вашего уровня данных и зачем писать код в своей сервисной модели только для того, чтобы нарушить тест? Вы обеспокоены тем, что вы или другие пользователи могли устанавливать объекты на недопустимые состояния на вашем уровне обслуживания? В этом случае вы должны изменить свой контракт, чтобы Product.Owner был readonly.

Вам лучше написать тест против вашего уровня данных, чтобы обеспечить правильное получение данных, а затем использовать модульные тесты для проверки бизнес-логики на уровне обслуживания. Если вас интересует более подробная информация об этом подходе, ответьте в комментариях.

Ответ 12

Посмотрите на все 4 намека, если вам кажется, что вы хотите сделать объект неизменным во время выполнения. Язык С# не поддерживает это. Это возможно только при рефакторинге класса продукта. Для рефакторинга вы можете принять подход IReadonlyProduct и защитить все сеттеры от вызова. Тем не менее, это все же позволяет модифицировать элементы контейнеров типа List<>, возвращаемые геттерами. Коллекция ReadOnly тоже не поможет. Только WPF позволяет изменять неизменность во время выполнения с помощью класса Freezable.

Итак, я вижу единственный правильный способ убедиться, что объекты имеют одинаковое содержимое, сравнивая их. Вероятно, самым простым способом было бы добавить атрибут [Serializable] для всех вовлеченных объектов и выполнить сериализацию с сопоставлением, как это предложил Фрэнк Швитерман.