Как сделать TDD для чего-то со многими перестановками?


15

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

Какой подход следует использовать, чтобы использовать TDD при создании системы, которая выдает много-много разных комбинаций результатов?


1
Общее качество системы AI обычно измеряется с помощью теста Precision-Recall с набором входных данных. Этот тест примерно на одном уровне с «интеграционными тестами». Как уже упоминалось, это больше похоже на «исследование алгоритмов, управляемых тестами», а не на « дизайн, управляемый тестами ».
Rwong

Пожалуйста, определите, что вы подразумеваете под «ИИ». Это область изучения больше, чем какой-либо конкретный тип программы. Для определенной реализации ИИ, вы обычно не можете тестировать некоторые типы вещей (например, эмерджентное поведение) через TDD.
Стивен Эверс

@ Snоrfus Я имею в виду, в самом общем, элементарном смысле, машину для принятия решений.
Николь

Ответы:


7

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

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

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

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

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

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

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

Вы можете спросить себя, зачем нужен этот «кодовый блат». Ну, во-первых, теперь вы можете посмеяться над поведением случайной части алгоритма, потому что Deciderтеперь есть зависимость, которая следует за IRandom«контрактом». Вы можете использовать для этого фальшивый фреймворк, но этот пример достаточно прост для написания кода:

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

Самое приятное то, что это может полностью заменить «фактическую» конкретную реализацию. Код становится легко тестировать следующим образом:

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

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


3

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

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


2

TDD не о тестировании, а о дизайне.

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

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

Изменить: я хотел добавить пример, но не было времени раньше.

Давайте рассмотрим алгоритм сортировки по месту. Мы можем пойти дальше и написать тесты, которые охватывают верхний конец массива, нижний конец массива и всевозможные странные комбинации в середине. Для каждого из нас мы должны были бы построить полный массив какого-либо объекта. Это займет время.

Или мы могли бы решить проблему в четырех частях:

  1. Пройдите через массив.
  2. Сравните выбранные товары.
  3. Поменять предметы.
  4. Координируйте вышеуказанные три.

Первая - единственная сложная часть проблемы, но, абстрагируя ее от остальных, вы сделали ее намного проще.

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

Третий невероятно легко проверить.

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

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

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


1

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

Теперь, в общем, все немного сложнее. Там действительно есть были ошибки , когда почтовая программа не будет доставлять сообщения электронной почты для пользователей , если они имеют «F» в их имени пользователя и день недели пятница. Но я считаю, что это попытка предвидеть такие странности. Ваш набор тестов должен обеспечить вам уверенность в том, что система делает то, что вы ожидаете, на ожидаемых вами результатах. Если в некоторых случаях вы делаете странные вещи, вы заметите это достаточно быстро после того, как попробуете первый случай, и затем вы можете написать тест специально для этого случая (который обычно также охватывает целый класс подобных случаев).


Учитывая, что вы генерируете 1000 входов случайным образом, как вы тогда тестируете выходы? Конечно, такой тест будет включать некоторую логику, которая сама по себе не проверена. Итак, вы тестируете тест? Как? Дело в том, что вы должны тестировать логику, используя переходы состояний - для заданного входа X выход должен быть Y. Тест, который включает в себя логику, подвержен ошибкам так же, как и логику, которую он тестирует. В логическом смысле, оправдание аргумента другим аргументом ставит вас на путь скептического регресса - вы должны сделать некоторые утверждения. Эти утверждения ваши тесты.
Изаки

0

Возьмите крайние случаи плюс некоторый случайный ввод

Чтобы взять пример сортировки:

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.