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

Единичное тестирование. Правильно ли я это делаю?

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

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

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

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

например. если 1 удаляется, объект 2 принимает значение id 1, объект 3 принимает значение id2.

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

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


Вот где я до сих пор. В основном у меня есть класс fruitManager со всеми методами, такими как deletefruit и т.д. Обычно он имеет список, но Ive изменил метод 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;
}
4b9b3361

Ответ 1

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

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

Итак:

1) [Test]
public void Fruit_Is_Removed_From_List_When_Deleted()

Когда этот тест написан, пройдите весь цикл TDD (выполните test = > red; напишите достаточно кода, чтобы он прошел = > зеленый, 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()

...

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

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

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

Рекомендуемое чтение Freeman/Pryce, растущее объектно-ориентированное программное обеспечение, ориентированное на тесты

Ответ 4

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

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

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

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

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

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

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

Ответ 5

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

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

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

Ответ 6

[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 выйдет из строя!

Ответ 7

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

[Спецификация] такая же, как [TestFixture] [Он] такой же, как [Test]

[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 и INCOMPLETE, но это хороший способ 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
   }
}

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

Итак, воила, для реализации на практике не требуется никакого REAL-кода... единственное, что нужно сейчас - это дисциплина.

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