Поблочное тестирование - я делаю его правильно?

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

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

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

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

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

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

Как я могу настроить это, чтобы сделать тест?


Вот то, где я имею до сих пор. В основном у меня есть 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;
}
16
задан Péter Török 18 April 2012 в 13:43
поделиться

7 ответов

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

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

Итак:

1) [Test]
public void Fruit_Is_Removed_From_List_When_Deleted()

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

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

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()

...

12
ответ дан 30 November 2019 в 21:27
поделиться

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

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

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

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

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

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

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

1
ответ дан 30 November 2019 в 21:27
поделиться

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

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

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

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

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

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

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

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

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

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

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

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

Recommended Reading Freeman/Pryce, Growing Object-Oriented Software, Guided By Tests

3
ответ дан 30 November 2019 в 21:27
поделиться

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

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

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

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

7
ответ дан 30 November 2019 в 21:27
поделиться

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

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

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

1
ответ дан 30 November 2019 в 21:27
поделиться
[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.

[Edit 1a]

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

1
ответ дан 30 November 2019 в 21:27
поделиться

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

[Спецификация] такая же, как [TestFixture] [It] is same as [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);
  }
}

Приведенная выше спецификация просто несистематическая и НЕПОЛНАЯ, но это хороший поведенческий 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
   }
}

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

Итак, вуаля, никакого РЕАЛЬНОГО кода на первых порах не требуется... единственное, что теперь нужно - это дисциплина.

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

1
ответ дан 30 November 2019 в 21:27
поделиться
Другие вопросы по тегам:

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