Как unit test абстрактные классы: расширяться с помощью заглушек?
Мне было интересно, как unit test абстрактные классы и классы, расширяющие абстрактные классы.
Должен ли я тестировать абстрактный класс, расширяя его, завершая абстрактные методы, а затем проверяю все конкретные методы? Затем проверяйте только методы, которые я переопределяю, и проверяем абстрактные методы в модульных тестах для объектов, расширяющих мой абстрактный класс?
Должен ли я иметь абстрактный тестовый пример, который можно использовать для тестирования методов абстрактного класса и расширить этот класс в моем тестовом примере для объектов, которые расширяют абстрактный класс?
Обратите внимание, что мой абстрактный класс имеет некоторые конкретные методы.
Ответы
Ответ 1
Напишите объект Mock и используйте их только для тестирования. Они обычно очень очень минимальны (наследуются от абстрактного класса) и не более. Затем в Unit Test можно вызвать абстрактный метод, который вы хотите проверить.
Вы должны протестировать абстрактный класс, который содержит некоторую логику, как и все остальные классы, которые у вас есть.
Ответ 2
Существует два способа использования абстрактных базовых классов.
-
Вы специализируетесь на своем абстрактном объекте, но все клиенты будут использовать производный класс через свой базовый интерфейс.
-
Вы используете абстрактный базовый класс для исключения дублирования в объектах вашего проекта, а клиенты используют конкретные реализации через свои собственные интерфейсы.!
Решение для 1 - Стратегия
Если у вас есть первая ситуация, у вас фактически есть интерфейс, определенный виртуальными методами абстрактного класса, которые реализуются ваши производные классы.
Вы должны подумать о том, чтобы сделать это реальным интерфейсом, изменив ваш абстрактный класс на конкретный, и возьмите экземпляр этого интерфейса в его конструкторе. Затем ваши производные классы становятся реализациями этого нового интерфейса.
Это означает, что теперь вы можете протестировать свой ранее абстрактный класс, используя макетный экземпляр нового интерфейса, и каждую новую реализацию через общедоступный интерфейс. Все просто и доступно для тестирования.
Решение для 2
Если у вас есть вторая ситуация, ваш абстрактный класс работает как вспомогательный класс.
Взгляните на содержащуюся в нем функциональность. Посмотрите, может ли кто-нибудь из них нажимать на объекты, которые обрабатываются, чтобы свести к минимуму это дублирование. Если у вас все еще осталось что-то, посмотрите на то, чтобы сделать его вспомогательным классом, чтобы ваша конкретная реализация взяла на себя их конструктор и удалила их базовый класс.
Это снова приводит к конкретным классам, которые просты и легко проверяются.
Как правило
Благоприятная сеть простых объектов над простой сетью сложных объектов.
Ключ к расширяемому тестируемому коду - это небольшие строительные блоки и независимая проводка.
Обновлено: Как обрабатывать смеси обоих?
Возможно, что базовый класс выполняет обе эти роли... то есть: он имеет открытый интерфейс и имеет защищенные вспомогательные методы. Если это так, вы можете разложить вспомогательные методы в один класс (сценарий2) и преобразовать дерево наследования в шаблон стратегии.
Если вы обнаружите, что у вас есть некоторые методы, которые ваш базовый класс реализует напрямую, а другие являются виртуальными, то вы все равно можете преобразовать дерево наследования в шаблон стратегии, но я бы также воспринял его как хороший показатель того, что обязанности не правильно выровнены, и может потребоваться рефакторинг.
Обновление 2: Абстрактные классы как степпинг (2014/06/12)
У меня была ситуация на днях, когда я использовал реферат, поэтому я хотел бы узнать, почему.
У нас есть стандартный формат для наших файлов конфигурации. Этот конкретный инструмент имеет 3 конфигурационных файла в этом формате. Мне нужен строго типизированный класс для каждого файла настроек, поэтому через инъекцию зависимостей класс может запросить настройки, о которых он заботился.
Я реализовал это, имея абстрактный базовый класс, который знает, как анализировать форматы файлов настроек и производные классы, которые раскрывают те же самые методы, но инкапсулировал местоположение файла настроек.
Я мог бы написать "SettingsFileParser", который 3 класса завернул, а затем передал в базовый класс, чтобы разоблачить методы доступа к данным. Я решил не делать этого пока, поскольку это приведет к 3 производным классам с большим количеством кода делегирования в них, чем что-либо еще.
Однако... поскольку этот код развивается, и потребители каждого из этих классов настроек становятся яснее. Каждый пользователь настройки запрашивает некоторые настройки и каким-то образом преобразует их (поскольку они представляют собой текст, он может обернуть их в объекты, преобразующие их в числа и т.д.). Поскольку это произойдет, я начну извлекать эту логику в методы манипулирования данными и оттеснять их на строго типизированные классы настроек. Это приведет к более высокоуровневому интерфейсу для каждого набора параметров, который, в конечном итоге, больше не знает, что он имеет дело с "настройками".
На этом этапе строго типизированные классы настроек больше не будут нуждаться в методах "getter", которые раскрывают базовую реализацию "настроек".
В этот момент я больше не хочу, чтобы их публичный интерфейс включал методы доступа к настройкам; поэтому я изменю этот класс, чтобы инкапсулировать класс парсера параметров, а не выводить из него.
Таким образом, класс Abstract: способ избежать кода делегирования на данный момент и маркер в коде, чтобы напомнить мне позже изменить дизайн. Я, возможно, никогда не доберусь до него, чтобы он мог хорошо жить... только код может сказать.
Я считаю, что это верно с любым правилом... например, "без статических методов" или "без личных методов". Они указывают на запах кода... и это хорошо. Это заставляет вас искать абстракцию, которую вы пропустили... и позволяет вам в то же время предоставлять ценность своему клиенту.
Я предполагаю, что такие правила, как этот, определяют ландшафт, где поддерживаемый код живет в долинах. Когда вы добавляете новое поведение, вам нравится дождь, приходящий на ваш код. Сначала вы кладете его туда, где он приземляется. Затем вы рефакторируете, чтобы силы хорошего дизайна подталкивали поведение, пока все не закончится в долинах.
Ответ 3
Что я делаю для абстрактных классов и интерфейсов: я пишу тест, который использует объект как конкретный. Но переменная типа X (X - абстрактный класс) не задана в тесте. Этот тестовый класс не добавляется в тестовый пакет, а подклассы его, которые имеют метод установки, который устанавливает переменную в конкретную реализацию X. Таким образом, я не дублирую тестовый код. Подклассы неиспользуемого теста могут при необходимости добавлять больше тестовых методов.
Ответ 4
Чтобы создать unit test специально для абстрактного класса, вы должны получить его для целей тестирования, результатов теста base.method() и предполагаемого поведения при наследовании.
Вы проверяете метод, вызывая его, чтобы проверить абстрактный класс, выполнив его...
Ответ 5
Если ваш абстрактный класс содержит конкретные функциональные возможности, которые имеют деловую ценность, я обычно тестирую его напрямую, создавая тестовый двойной, который заглушает абстрактные данные, или используя насмешливую структуру, чтобы сделать это для меня. Какой я выбираю, зависит от того, нужно ли мне писать тестовые реализации абстрактных методов или нет.
Наиболее распространенным сценарием, в котором я должен это сделать, является использование Шаблон метода шаблона, например, когда я создавая какую-то расширяемую структуру, которая будет использоваться сторонней стороной. В этом случае абстрактный класс определяет алгоритм, который я хочу проверить, поэтому имеет смысл тестировать абстрактную базу, чем конкретную реализацию.
Однако я считаю важным, чтобы эти тесты были сосредоточены на конкретных реализациях реальной бизнес-логики; вы не должны unit test детали реализации абстрактного класса, потому что в итоге вы получите хрупкие тесты.
Ответ 6
один способ - написать абстрактный тестовый пример, соответствующий вашему абстрактному классу, а затем написать конкретные тестовые примеры, которые подклассы вашего абстрактного тестового примера. сделайте это для каждого конкретного подкласса вашего исходного абстрактного класса (т.е. иерархия вашего тестового случая отражает вашу иерархию классов). см. Проверка интерфейса в книге рецептов junit: http://safari.informit.com/9781932394238/ch02lev1sec6.
также см. "Суперкласс класса" в шаблонах xUnit: http://xunitpatterns.com/Testcase%20Superclass.html
Ответ 7
Я бы возражал против "абстрактных" тестов. Я думаю, что тест является конкретной идеей и не имеет абстракции. Если у вас есть общие элементы, поместите их в вспомогательные методы или классы для каждого пользователя.
Что касается тестирования абстрактного тестового класса, убедитесь, что вы спрашиваете себя, что именно вы тестируете. Существует несколько подходов, и вы должны выяснить, что работает в вашем сценарии. Вы пытаетесь проверить новый метод в своем подклассе? Затем ваши тесты будут взаимодействовать только с этим методом. Вы тестируете методы в базовом классе? Затем, возможно, есть отдельный прибор только для этого класса и каждый тест каждый раз проверяет как можно больше тестов.
Ответ 8
Это шаблон, который я обычно соблюдаю при настройке жгута для тестирования абстрактного класса:
public abstract class MyBase{
/*...*/
public abstract void VoidMethod(object param1);
public abstract object MethodWithReturn(object param1);
/*,,,*/
}
И версия, которую я использую при тестировании:
public class MyBaseHarness : MyBase{
/*...*/
public Action<object> VoidMethodFunction;
public override void VoidMethod(object param1){
VoidMethodFunction(param1);
}
public Func<object, object> MethodWithReturnFunction;
public override object MethodWithReturn(object param1){
return MethodWihtReturnFunction(param1);
}
/*,,,*/
}
Если абстрактные методы вызывают, когда я этого не ожидаю, тесты терпят неудачу. При организации тестов я могу легко исключить абстрактные методы с помощью лямбда, которые выполняют утверждения, бросают исключения, возвращают разные значения и т.д.
Ответ 9
Если конкретные методы вызывают любой из абстрактных методов, которые стратегия не будет работать, и вы хотите протестировать поведение каждого дочернего класса отдельно. В противном случае, расширяя его и завершая абстрактные методы, как вы описали, должно быть хорошо, снова при условии, что абстрактные классы конкретных методов отделены от дочерних классов.
Ответ 10
Я предполагаю, что вы могли бы протестировать базовую функциональность абстрактного класса... Но вам, вероятно, было бы лучше, расширив класс без переопределения каких-либо методов и сделав минимальное усилие насмешкой над абстрактными методами.
Ответ 11
Одной из основных мотивов использования абстрактного класса является включение полиморфизма в ваше приложение - т.е. вы можете заменить другую версию во время выполнения. Фактически, это почти то же самое, что использование интерфейса, кроме абстрактного класса, обеспечивает некоторую общую сантехнику, часто называемую шаблоном шаблона.
С точки зрения единичного тестирования можно рассмотреть две вещи:
-
Взаимодействие вашего абстрактного класса с связанными с ним классами. Использование макетной системы тестирования идеально подходит для этого сценария, так как показывает, что ваш абстрактный класс хорошо работает с другими.
-
Функциональность производных классов. Если у вас есть пользовательская логика, написанная для ваших производных классов, вы должны изолировать эти классы.
edit: RhinoMocks - это потрясающая макетная система тестирования, которая может генерировать макетные объекты во время выполнения, динамически исходя из вашего класса. Этот подход может сэкономить вам бесчисленные часы производных классов с кодированием вручную.
Ответ 12
Сначала, если абстрактный класс содержал какой-то конкретный метод, я думаю, вы должны сделать это, рассмотрев этот пример
public abstract class A
{
public boolean method 1
{
// concrete method which we have to test.
}
}
class B extends class A
{
@override
public boolean method 1
{
// override same method as above.
}
}
class Test_A
{
private static B b; // reference object of the class B
@Before
public void init()
{
b = new B ();
}
@Test
public void Test_method 1
{
b.method 1; // use some assertion statements.
}
}
Ответ 13
После ответа @patrick-desjardins я реализовал абстрактный и его класс реализации вместе с @Test
следующим образом:
Абстрактный класс - ABC.java
import java.util.ArrayList;
import java.util.List;
public abstract class ABC {
abstract String sayHello();
public List<String> getList() {
final List<String> defaultList = new ArrayList<>();
defaultList.add("abstract class");
return defaultList;
}
}
Поскольку абстрактные классы не могут быть созданы, но могут быть разделены на подклассы, конкретный класс DEF.java выглядит следующим образом:
public class DEF extends ABC {
@Override
public String sayHello() {
return "Hello!";
}
}
Класс @Test для тестирования как абстрактного, так и неабстрактного метода:
import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
import org.junit.Test;
public class DEFTest {
private DEF def;
@Before
public void setup() {
def = new DEF();
}
@Test
public void add(){
String result = def.sayHello();
assertThat(result, is(equalTo("Hello!")));
}
@Test
public void getList(){
List<String> result = def.getList();
assertThat((Collection<String>) result, is(not(empty())));
assertThat(result, contains("abstract class"));
}
}
Ответ 14
Если абстрактный класс подходит для вашей реализации, протестируйте (как предложено выше) производный конкретный класс. Ваши предположения верны.
Чтобы избежать путаницы в будущем, имейте в виду, что этот конкретный тестовый класс - не подделка, а подделка.
В строгом смысле, макет определяется следующими характеристиками:
- Макет используется вместо каждой зависимости тестируемого предметного класса.
- Макет - это псевдо-реализация интерфейса (вы можете вспомнить, что, как правило, зависимости должны быть объявлены как интерфейсы; тестируемость является одной из основных причин этого)
- Поведение членов-имитаторов интерфейса - будь то методы или свойства - предоставляется во время тестирования (опять же, с использованием среды моделирования). Таким образом, вы избегаете связи тестируемой реализации с реализацией ее зависимостей (которые должны иметь свои собственные дискретные тесты).