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


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

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
      }
   }
}

Рассмотрим простой единичный тест:

[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 и returnProduct - фактически та же ссылка.

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

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

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


12


источник


Ответы:


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

Тест на самом деле должен выглядеть так:

[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);
}

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


9



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

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

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


3



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

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

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

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

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

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

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

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

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

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

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

EDIT: Я отвечаю на это из-за противоречий, заданных в вопросе. Учитывая свободный выбор, я бы предложил не использовать EntityGenerator для создания экземпляров тестовых продуктов, а вместо этого создавать их «вручную» и использовать сравнение равенства. Или более прямым, сравните поля возвращаемого продукта с конкретными (жестко закодированными) значениями в тесте, опять же, без использования EntityGenerator в тесте.


3



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

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

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

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

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

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

Ваши потребители продукта (МОДЕЛЬ) являются единственными, кто заботится об осуществлении 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();
}

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


2



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

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

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

Так я бы сделал последнее с 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();
}

1



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

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

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

Я написал некоторые служебные функции ... Assert2.IsSameValue (ожидаемый, актуальный), который функционирует как Assert.AreEqual () NUnit, за исключением того, что он сериализуется через 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;
        });
    }
}

1



Ну, один из способов - обмануть продукт, а не фактический продукт. Не пытайтесь повлиять на продукт, делая его строгим. (Я предполагаю, что вы используете 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();
}

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


0



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

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

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


0



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

Такие как

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

Наслаждаться.


0



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

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

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


0



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

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

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

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

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

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


0