Модульное тестирование - правильно ли я делаю? - PullRequest
16 голосов
/ 05 мая 2010

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

Трудно объяснить, но, в основном, скажем, например, у меня есть объекты Fruit со свойствами, такими как id, color и cost. (Все данные, хранящиеся в текстовом файле, полностью игнорируют логику базы данных и т. Д.)

    FruitID FruitName   FruitColor  FruitCost
    1         Apple       Red         1.2
    2         Apple       Green       1.4
    3         Apple       HalfHalf    1.5

Это все только для примера. Но допустим, у меня есть коллекция Fruit (это List<Fruit>) объектов в этой структуре. И моя логика скажет, что нужно переупорядочить фрукты в коллекции, если фрукт удален (именно так должно быть решение).

например. если 1 удалено, объект 2 получает фруктовый идентификатор 1, объект 3 получает фруктовый идентификатор 2.

Теперь я хочу проверить написанный код, который выполняет переупорядочение и т. Д.

Как я могу настроить это для проведения теста?


Вот где у меня так далеко. В основном у меня есть класс fruitManager со всеми методами, такими как deletefruit и т. Д. У него обычно есть список, но я изменил метод hte, чтобы проверить его, чтобы он принимал список и информацию об удаляемом фрукте, а затем возвращает список.

Юнит-тестирование мудро: Я в основном делаю это правильно, или я неправильно понял? а затем я проверяю удаление различных ценных объектов / наборов данных, чтобы убедиться, что метод работает правильно.


[Test]
public void DeleteFruit()
{
    var fruitList = CreateFruitList();
    var fm = new FruitManager();

    var resultList = fm.DeleteFruitTest("Apple", 2, fruitList);

    //Assert that fruitobject with x properties is not in list ? how
}

private static List<Fruit> CreateFruitList()
{
    //Build test data
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...};
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...};
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...};

    var fruitList = new List<Fruit> {f01, f02, f03};
    return fruitList;
}

Ответы [ 7 ]

12 голосов
/ 05 мая 2010

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

Что вы ожидаете от метода Delete () в первую очередь? Если бы вы отправили Удалить «продукт» в течение 10 минут, что бы включило необоротное поведение? Ну ... наверное, это удаляет элемент.

Итак:

1) [Test]
public void Fruit_Is_Removed_From_List_When_Deleted()

Когда написан этот тест, пройдите весь цикл TDD (выполните test => red; напишите достаточно кода, чтобы он прошел => green; refactor => green)

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

2) [Test]
public void Invalid_Fruit_Changes_Nothing_When_Deleted()

Следующее, что вы указали, это то, что идентификаторы должны быть переставлены при удалении фрукта:

3) [Test]
public void Fruit_Ids_Are_Reordered_When_Fruit_Is_Deleted()

Что поставить в этот тест? Что ж, просто настройте базовый, но представительный контекст, который докажет, что ваш метод ведет себя так, как ожидалось.

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

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

4) [Test]
public void Fruit_Ids_Arent_Reordered_When_Last_Fruit_Is_Deleted()

5) [Test]
[ExpectedException]
public void Exception_Is_Thrown_When_Fruit_List_Is_Empty()

...

7 голосов
/ 05 мая 2010

Прежде чем вы начнете писать свой первый тест, у вас должно быть приблизительное представление о структуре / дизайне вашего приложения, интерфейсах и т. Д. Этап проектирования часто подразумевается как TDD.

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

В любом случае, после того, как первый эскиз проекта готов, TDD может использоваться как для проверки поведения, так и для проверки надежности / удобства использования самого проекта . Вы можете начать писать свой первый модульный тест, а затем понять: «О, на самом деле это довольно неловко делать с интерфейсом, который я представлял» - тогда вы вернетесь и перепроектируете интерфейс. Это итеративный подход.

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

3 голосов
/ 05 мая 2010

Модули юнит-тестирование: правильно ли я делаю это, или у меня неправильное представление?

Вы пропустили лодку.

Я не совсем понимаю, как проходит тест перед кодом, если вы не знаете, какие структуры и как вы храните данные

К этому вопросу, я думаю, вам нужно вернуться, если вы хотите, чтобы идеи имели смысл.

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

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

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

Очень трудно достичь этого, когда вы используете примеры, ориентированные на реализацию (например, написание теста для контейнера). То, что вам нужно, это хорошо ограниченная игрушка, независимая от реализации.

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

Так что вы, возможно, пишете несколько простых тестов с именами пользователей и паролями. И, поступая так, вы понимаете, что секреты не должны быть ограничены строками, но вы должны иметь возможность создавать секрет из чего-либо сериализуемого, и что, возможно, доступ не универсален, но ограничен (это касается диспетчера аутентификации) ? может и нет) и о, вы захотите продемонстрировать, что секреты хранятся в безопасности ....

Конечно, вы можете использовать тот же подход для контейнеров. Но я думаю, вам будет проще «получить», если вы начнете с проблемы пользователя / бизнеса, а не с проблемы реализации.

Модульные тесты, которые проверяют конкретную реализацию («У нас здесь ошибка поста забора?») Имеют значение. Процесс их создания во многом похож на «угадай ошибку, напиши тест для проверки ошибки, отреагируй, если тест провалится». Эти тесты, как правило, не вносят свой вклад в ваш дизайн - у вас гораздо больше шансов клонировать блок кода и изменить некоторые входные данные. Однако часто бывает так, что когда модульные тесты следуют за реализацией, их часто сложно написать и они требуют больших затрат на запуск («зачем мне нужно загружать три библиотеки и запускать удаленный веб-сервер, чтобы проверить ошибку забора в моем цикле for»). ? ").

Рекомендуемое чтение Фриман / Прайс, Растущее объектно-ориентированное программное обеспечение, руководствуясь тестами

1 голос
/ 19 мая 2010

Начните с интерфейса, имейте конкретную реализацию скелета. Для каждого метода / свойства / события / конструктора существует ожидаемое поведение. Начните со спецификации для первого поведения и завершите ее:

[Спецификация] такая же, как [TestFixture] [Это] так же, как [тест]

[Specification]
When_fruit_manager_has_delete_called_with_existing_fruit : FruitManagerSpecifcation
{
  private IEnumerable<IFruit> _fruits;

  [It]
  public void Should_remove_the_expected_fruit()
  {
    Assert.Inconclusive("Please implement");
  }

  [It]
  public void Should_not_remove_any_other_fruit()
  {
    Assert.Inconclusive("Please implement");
  }

  [It]
  public void Should_reorder_the_ids_of_the_remaining_fruit()
  {
    Assert.Inconclusive("Please implement");
  }

  /// <summary>
  /// Setup the SUT before creation
  /// </summary>
  public override void GivenThat()
  {
     _fruits = new List<IFruit>();

     3.Times(_fruits.Add(Mock<IFruit>()));

     this._fruitToDelete = _fruits[1];

     // this fruit is injected in th Sut
     Dep<IEnumerable<IFruit>>()
                .Stub(f => ((IEnumerable)f).GetEnumerator())
                .Return(this.Fruits.GetEnumerator())
                .WhenCalled(mi => mi.ReturnValue = this._fruits.GetEnumerator());

  }

  /// <summary>
  /// Delete a fruit
  /// </summary>
  public override void WhenIRun()
  {
    Sut.Delete(this._fruitToDelete);
  }
}

Вышеприведенная спецификация является просто adhoc и НЕПОЛНОЙ, но это подход TDD с хорошим поведением для подхода к каждой единице / спецификации.

Здесь будет часть невыполненной SUT, когда вы впервые начнете работать над ней:

public interface IFruitManager
{
  IEnumerable<IFruit> Fruits { get; }

  void Delete(IFruit);
}

public class FruitManager : IFruitManager
{
   public FruitManager(IEnumerable<IFruit> fruits)
   {
     //not implemented
   }

   public IEnumerable<IFruit> Fruits { get; private set; }

   public void Delete(IFruit fruit)
   {
    // not implemented
   }
}

Итак, как вы можете видеть, настоящий код не написан. Если вы хотите завершить эту первую спецификацию «Когда _...», вам сначала нужно выполнить [ConstructorSpecification] When_fruit_manager_is_injected_with_fruit (), потому что введенные фрукты не назначаются свойству Fruits.

Так, вуаля, на самом деле не нужно реализовывать РЕАЛЬНЫЙ код ... единственное, что сейчас нужно, это дисциплина.

Одна вещь, которая мне нравится в этом, это то, что если вам нужны дополнительные классы во время реализации текущего SUT, вам не нужно реализовывать их перед тем, как реализовывать FruitManager, потому что вы можете просто использовать mock, как, например, ISomeDependencyNeeded ... и когда вы заполняете Fruit Manager, а затем можете работать с классом SomeDependencyNeeded. Довольно злой.

1 голос
/ 05 мая 2010
[Test]
public void DeleteFruit()
{
    var fruitList = CreateFruitList();
    var fm = new FruitManager(fruitList);

    var resultList = fm.DeleteFruit(2);

    //Assert that fruitobject with x properties is not in list
    Assert.IsEqual(fruitList[2], fm.Find(2));
}

private static List<Fruit> CreateFruitList()
{
    //Build test data
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...};
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...};
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...};

    return new List<Fruit> {f01, f02, f03};
}

Вы можете попробовать добавить зависимости в список фруктов. Объект «Фруктовый менеджер» - это магазин. Поэтому, если у вас есть операция удаления, вам нужна операция извлечения.

Что касается переупорядочения, хотите ли вы, чтобы оно происходило автоматически, или вам нужна операция на курорте. Автоматически также может быть, как только происходит операция удаления, или только при извлечении. Это деталь реализации. Об этом можно сказать намного больше. Хорошим началом работы с этим конкретным примером станет использование Design By Contract.

[Изменить 1a]

Также вы можете подумать, почему вы тестируете конкретные реализации Fruit . FruitManager должен управлять абстрактным понятием под названием Fruit. Вам нужно следить за преждевременными деталями реализации, если вы не хотите идти по пути использования DTO, но проблема в том, что Fruit может в конечном итоге измениться с объекта с геттерами на объект с реальным поведением. Теперь не только ваши тесты на Fruit не пройдут, но и FruitManager не пройдут!

1 голос
/ 05 мая 2010

Поскольку вы используете C #, я предполагаю, что NUnit - это ваша тестовая среда. В этом случае у вас есть ряд утверждений Assert [..].

Что касается специфики вашего кода: я бы не переназначил идентификаторы и не изменил бы состав оставшихся объектов Fruit при манипулировании списком. Если вам нужен идентификатор для отслеживания положения объекта в списке, используйте вместо него .IndexOf ().

С TDD я нахожу, что сначала писать тест часто довольно сложно - я заканчиваю тем, что пишу код первым (код или строка хаков, которые есть). Хороший трюк - взять этот «код» и использовать его в качестве теста. Затем напишите свой действительный код снова , немного по-другому. Таким образом, у вас будет два разных куска кода, которые выполняют одно и то же - меньше шансов сделать одну и ту же ошибку в рабочем и тестовом коде. Кроме того, необходимость найти второе решение для той же проблемы может показать вам недостатки в вашем первоначальном подходе и привести к улучшению кода.

1 голос
/ 05 мая 2010

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

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

  • Сначала напишите метод проверки.Вы можете сделать это, как только узнаете, что у вас будет список фруктов и что в этом списке все фрукты будут иметь последовательные идентификаторы (это все равно, что проверять, отсортирован ли список).Код для удаления не должен быть написан для этого, плюс вы можете позже использовать его f.ex.в коде вставки модульного тестирования.

  • Затем создайте группу различных (возможно случайных) тестовых списков (пустой размер, средний размер, большой размер).Это также не требует предварительного кода для удаления.

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...