Когда я должен дразнить?

PECS (сокращение от «Производитель extends и Consumer super») объясняется: принципом «Get and Put»

«Get And Put» (из Java Generics and Collections)

Указывает, что

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

Давайте рассмотрим его на примере:

1. For Extends Wildcard (получить значения, т. Е. Producer extends)

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

public static double sum(Collection<? extends Number> nums) {
   double s = 0.0;
   for (Number num : nums) 
      s += num.doubleValue();
   return s;
}

Назовем метод:

List<Integer>ints = Arrays.asList(1,2,3);
assert sum(ints) == 6.0;
List<Double>doubles = Arrays.asList(2.78,3.14);
assert sum(doubles) == 5.92;
List<Number>nums = Arrays.<Number>asList(1,2,2.78,3.14);
assert sum(nums) == 8.92;

Поскольку метод sum() использует extends, все следующие вызовы являются законными. Первые два вызова не были бы законными, если бы расширения не использовались.

ИСКЛЮЧЕНИЕ : вы не можете помещать что-либо в тип, объявленный с помощью подстановочного знака extends, за исключением значения null, который относится к каждому ссылочному типу:

List<Integer> ints = new ArrayList<Integer>();
ints.add(1);
ints.add(2);
List<? extends Number> nums = ints;
nums.add(null);  // ok
assert nums.toString().equals("[1, 2, null]");

2. Для Super Wildcard (put values ​​ie Consumer super)

Вот метод, который принимает набор чисел и int n, и помещает первые n целые числа, начиная с нуля, в сбор:

public static void count(Collection<? super Integer> ints, int n) {
    for (int i = 0; i < n; i++) ints.add(i);
}

Давайте назовем метод:

List<Integer>ints = new ArrayList<Integer>();
count(ints, 5);
assert ints.toString().equals("[0, 1, 2, 3, 4]");
List<Number>nums = new ArrayList<Number>();
count(nums, 5); nums.add(5.0);
assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]");
List<Object>objs = new ArrayList<Object>();
count(objs, 5); objs.add("five");
assert objs.toString().equals("[0, 1, 2, 3, 4, five]");

Поскольку метод count() использует super, все следующие звонки законны: последние два вызова не были бы законными, если бы супер не использовался.

ИСКЛЮЧЕНИЕ : вы не можете получить что-либо из типа, объявленного с помощью super за исключением значения типа Object, которое является супертипом каждого ссылочного типа:

List<Object> objs = Arrays.<Object>asList(1,"two");
List<? super Integer> ints = objs;
String str = "";
for (Object obj : ints) str += obj.toString();
assert str.equals("1two");

3. Когда оба Get и Put, не используйте wildcard

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

public static double sumCount(Collection<Number> nums, int n) {
   count(nums, n);
   return sum(nums);
}
122
задан Community 23 May 2017 в 11:55
поделиться

4 ответа

Модульный тест должен протестировать единственный путь выполнения кода через отдельный метод. Когда осуществление метода передает за пределами того метода в другой объект, и назад снова, у Вас есть зависимость.

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

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

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

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

TL; DR: Дразните каждую зависимость Ваши касания модульного теста.

116
ответ дан Drew Stephens 24 November 2019 в 01:21
поделиться

Эмпирическое правило:

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

47
ответ дан Orion Edwards 24 November 2019 в 01:21
поделиться

Фиктивные объекты полезны, когда Вы хотите к [1 146] тестовые взаимодействия между классом под тестом и конкретным интерфейсом.

, Например, мы хотим протестировать тот метод sendInvitations(MailServer mailServer) вызовы MailServer.createMessage() точно однажды, и также звонит MailServer.sendMessage(m) точно однажды, и никакие другие методы не называют на эти MailServer интерфейс. Это - когда мы можем использовать фиктивные объекты.

С фиктивными объектами, вместо того, чтобы передать реальное MailServerImpl, или тест TestMailServer, мы можем передать ложную реализацию эти MailServer интерфейс. Прежде чем мы передадим насмешку MailServer, мы "обучаем" ее, так, чтобы она знала что вызовы метода ожидать и что возвращаемые значения возвратиться. В конце фиктивный объект утверждает, что все ожидаемые методы назвали как ожидалось.

Это звучит хорошим в теории, но существуют также некоторые оборотные стороны.

Ложные недостатки

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

Вот пример в псевдокоде. Давайте предположим, что мы создали MySorter класс, и мы хотим протестировать его:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

(В этом примере мы предполагаем, что это не конкретный алгоритм сортировки, такой как быстрая сортировка, что мы хотим протестировать; в этом случае последний тест на самом деле был бы допустим.)

В таком экстремальном примере очевидно, почему последний пример является неправильным. Когда мы изменяем реализацию [1 111], первый тест делает отличную работу по проверке мы все еще вид правильно, который является смыслом тестов - они позволяют нам изменять код безопасно. С другой стороны, последний тест всегда повреждения и это активно вредны; это препятствует рефакторингу.

Насмешки как тупики

Ложные платформы часто позволяют также менее строгое использование, где мы не должны определять точно, сколько раз методов нужно назвать и какие параметры ожидаются; они позволяют создавать фиктивные объекты, которые используются в качестве [1 122] тупики .

Позволяют нам предположить, что у нас есть метод sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer), что мы хотим протестировать. Эти PdfFormatter объект может использоваться для создания приглашения. Вот тест:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

В этом примере, мы действительно не заботимся об эти PdfFormatter объект, таким образом, мы просто обучаем его бесшумно принимать любой вызов и возвращать некоторые разумные консервированные возвращаемые значения для всех методов, который sendInvitation(), оказывается, заходит в эту точку. Как мы придумывали точно этот список методов для обучения? Мы просто запустили тест и продолжали добавлять методы, пока тест не передал. Заметьте, что мы обучили тупик отвечать на метод, не имея подсказки, почему он должен назвать его, мы просто добавили все, на что жаловался тест. Мы счастливы, тестовые передачи.

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

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

, Как зафиксировать это? Легко:

  • Попытка с помощью реальных классов вместо насмешек, когда это возможно. Используйте реальное PdfFormatterImpl. Если это не возможно, измените реальные классы для позволения. Быть неспособностью для использования класса в тестах обычно указывает на некоторые проблемы с классом. Решение проблем является взаимовыгодной ситуацией - Вы зафиксировали класс, и у Вас есть более простой тест. С другой стороны, фиксация его и использование насмешек являются безнадежной ситуацией - Вы не зафиксировали реальный класс, и Вы имеете более сложный, меньше читаемых тестов, которые препятствуют дальнейшим рефакторингам.
  • Попытка, создающая простую тестовую реализацию интерфейса вместо того, чтобы дразнить это в каждом тесте и использовании этот тестовый класс во всех Ваших тестах. Создайте TestPdfFormatter, который ничего не делает. Тем путем можно изменить его однажды для всех тестов, и тесты не нарушены долгими установками, где Вы обучаете свои тупики.

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

еще Для некоторых деталей о недостатках насмешек см. также Фиктивные объекты: Недостатки и Варианты использования .

152
ответ дан palacsint 24 November 2019 в 01:21
поделиться

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

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

А большой подкаст по теме может быть найден здесь

3
ответ дан palacsint 24 November 2019 в 01:21
поделиться
Другие вопросы по тегам:

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