Поблочное тестирование, дразня - простой случай: Сервис - Репозиторий

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

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: данный тест плох, потому что продуктом и returnedProduct является на самом деле та же ссылка.

ПОДСКАЗКА 2: Реализующие участники равенства (object.equals) не являются решением.

ПОДСКАЗКА 3: Что касается теперь, я создаю клон экземпляра продукта (expectedProduct) с AutoMapper - но мне не нравится это решение.

ПОДСКАЗКА 4: я не тестирую это, SUT НЕ делает sth. Я пытаюсь протестировать тот SUT, ДЕЙСТВИТЕЛЬНО возвращает тот же объект, как он возвращается из репозитория.

12
задан rafek 22 May 2010 в 09:54
поделиться

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
ответ дан 2 December 2019 в 18:52
поделиться

Ухххххххххх ...................

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

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

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

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

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

Ваши потребители продукта (МОДЕЛЬ) - единственные, кого волнует реализация продукта .

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

[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
ответ дан 2 December 2019 в 18:52
поделиться

Ну, один из способов - передать имитацию продукта, а не сам продукт. Убедитесь, что ничто не может повлиять на продукт, сделав его строгим. (Я предполагаю, что вы используете 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
ответ дан 2 December 2019 в 18:52
поделиться

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

Например,

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
ответ дан 2 December 2019 в 18:52
поделиться

Один из способов мышления модульных тестов - это закодированные спецификации. Когда вы используете 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
ответ дан 2 December 2019 в 18:52
поделиться

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

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

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

0
ответ дан 2 December 2019 в 18:52
поделиться

Почему бы вам не издеваться над продуктом , а также над репозиторием продуктов ?

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

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

3
ответ дан 2 December 2019 в 18:52
поделиться

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

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

0
ответ дан 2 December 2019 в 18:52
поделиться

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

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

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

Вот как я бы сделал последнее с помощью 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
ответ дан 2 December 2019 в 18:52
поделиться

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

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

Больше нет посредников, больше нет проблем с несоответствием, больше нет необходимости проверять это несоответствие.

0
ответ дан 2 December 2019 в 18:52
поделиться

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

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

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

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

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

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

0
ответ дан 2 December 2019 в 18:52
поделиться

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

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

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

Я написал несколько полезных функций... Assert2.IsSameValue(expected,actual), которая работает как NUnit's 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;
        });
    }
}
1
ответ дан 2 December 2019 в 18:52
поделиться
Другие вопросы по тегам:

Похожие вопросы: