как тестировать абстрактные классы: дополнить заглушками?

<p>мне было интересно, как тестировать абстрактные классы и классы, расширяющие абстрактные классы.</p><p>должен ли я тестировать абстрактный класс, расширяя его, заглушая абстрактные методы, а затем тестируя все конкретные методы? тогда только тестируйте методы, которые я переопределяю, и тестируйте абстрактные методы в модульных тестах для объектов, расширяющих мой абстрактный класс?</p><p>должен ли я иметь абстрактный контрольный пример, который можно использовать для проверки методов абстрактного класса, и расширить этот класс в моем тестовом примере для объектов, расширяющих абстрактный класс?</p><p>обратите внимание, что мой абстрактный класс имеет несколько конкретных методов.</p>

<p>напишите объект mock и используйте его только для тестирования. обычно они очень, очень, очень минимальны (наследуются от абстрактного класса) и не более. затем в модульном тесте вы можете вызвать абстрактный метод, который вы хотите протестировать.</p><p>вы должны протестировать абстрактный класс, который содержит некоторую логику, как и все остальные классы, которые у вас есть.</p>

<p>есть два способа использования абстрактных базовых классов.</p><ol> <li> <p>вы специализируете свой абстрактный объект, но все клиенты будут использовать производный класс через его базовый интерфейс.</p> </li> <li> <p>вы используете абстрактный базовый класс, чтобы исключить дублирование объектов в вашем проекте, а клиенты используют конкретные реализации через свои собственные интерфейсы.</p> </li> </ol><hr><p><strong> решение для 1 - шаблон стратегии </strong></p><p><img src="https://i.stack.imgur.com/bu4vy.png" alt="option1"></p><p>если у вас первая ситуация, то у вас фактически есть интерфейс, определенный виртуальными методами в абстрактном классе, которые реализуют ваши производные классы.</p><p>вам следует подумать о том, чтобы сделать это реальным интерфейсом, изменить конкретный класс на конкретный и взять экземпляр этого интерфейса в его конструкторе. затем ваши производные классы становятся реализациями этого нового интерфейса.</p><p><img src="https://i.stack.imgur.com/wppqa.png" alt="imotor"></p><p>это означает, что теперь вы можете протестировать свой ранее абстрактный класс, используя фиктивный экземпляр нового интерфейса, а каждую новую реализацию - через общедоступный интерфейс. все просто и проверяемо.</p><hr><p><strong> решение для 2 </strong></p><p>если у вас вторая ситуация, тогда ваш абстрактный класс работает как вспомогательный класс.</p><p><img src="https://i.stack.imgur.com/abrgo.png" alt="abstracthelper"></p><p>посмотрите на функциональность, которую он содержит. посмотрите, может ли что-либо из этого быть помещено на объекты, которыми манипулируют, чтобы минимизировать это дублирование. если у вас все еще есть что-то, посмотрите, как сделать это вспомогательным классом, который ваша конкретная реализация использует в своем конструкторе, и удалите его базовый класс.</p><p><img src="https://i.stack.imgur.com/eysjq.png" alt="motor helper"></p><p>это снова приводит к конкретным классам, которые просты и легко тестируемы.</p><hr><p><strong> как правило </strong></p><p>пользуйтесь сложной сетью простых объектов над простой сетью сложных объектов.</p><p>ключом к расширяемому тестируемому коду являются небольшие строительные блоки и независимая проводка.</p><hr><p><strong> обновлено: как обрабатывать смеси обоих? </strong></p><p>можно иметь базовый класс, выполняющий обе эти роли ... т.е. он имеет открытый интерфейс и имеет защищенные вспомогательные методы. если это так, то вы можете выделить вспомогательные методы в один класс (script2) и преобразовать дерево наследования в шаблон стратегии.</p><p>если вы обнаружите, что у вас есть некоторые методы, которые ваш базовый класс реализует напрямую, а другие являются виртуальными, то вы все равно можете преобразовать дерево наследования в шаблон стратегии, но я бы также использовал его как хороший индикатор того, что обязанности не правильно выровнены, и может нужен рефакторинг.</p><hr><p><strong> обновление 2: абстрактные классы как ступенька (2014/06/12) </strong></p><p>у меня была ситуация, когда я использовал реферат, поэтому я хотел бы выяснить, почему.</p><p>у нас есть стандартный формат для наших файлов конфигурации. этот конкретный инструмент имеет 3 файла конфигурации в этом формате. я хотел иметь строго типизированный класс для каждого файла настроек, чтобы через внедрение зависимостей класс мог запрашивать настройки, о которых он заботился.</p><p>я реализовал это, имея абстрактный базовый класс, который знает, как анализировать форматы файлов настроек и производные классы, которые предоставляют те же методы, но инкапсулируют местоположение файла настроек.</p><p>я мог бы написать "settingsfileparser", который обернул 3 класса, а затем делегировать его базовому классу для предоставления методов доступа к данным. я решил пока не делать этого <em> </em>, так как это приведет к 3 производным классам с большим <em> делегированием </em> кода в них больше всего на свете.</p><p>однако ... как этот код развивается, и потребители каждого из этих классов настроек становятся понятнее. каждую настройку пользователи будут запрашивать некоторые настройки и каким-то образом преобразовывать их (поскольку настройки являются текстовыми, они могут обернуть их в объекты, преобразовать их в числа и т.д.). когда это произойдет, я начну извлекать эту логику в методы манипулирования данными и возвращать их обратно в строго типизированные классы настроек. это приведет к более высокоуровневому интерфейсу для каждого набора настроек, который в конечном итоге перестанет осознавать, что имеет дело с "настройками".</p><p>на этом этапе строго типизированные классы настроек больше не будут нуждаться в методах "получения", которые предоставляют базовую реализацию "настроек".</p><p>в этот момент я бы больше не хотел, чтобы их общедоступный интерфейс включал методы доступа к настройкам; поэтому я изменю этот класс, чтобы инкапсулировать класс анализатора настроек, а не наследовать от него.</p><p>таким образом, класс abstract: для меня способ избежать кода делегирования на данный момент и маркер в коде, чтобы напомнить мне об изменении дизайна позже. я, возможно, никогда не доберусь до этого, поэтому он может жить долго ... только код может сказать.</p><p>я считаю, что это верно для любого правила ... типа "нет статических методов" или "нет частных методов". они указывают запах в коде ... и это хорошо. это заставляет вас искать абстракцию, которую вы пропустили ... и позволяет в то же время обеспечивать ценность для вашего клиента.</p><p>я представляю себе правила, подобные этому, определяющие ландшафт, в котором обслуживаемый код живет в долинах. когда вы добавляете новое поведение, это похоже на дождь в вашем коде. сначала вы помещаете его туда, где он приземляется. затем вы делаете рефакторинг, чтобы позволить силам хорошего замысла подтолкнуть поведение, пока все не окажется в долинах.</p>

<p>что я делаю для абстрактных классов и интерфейсов, так это следующее: я пишу тест, который использует объект как конкретный. но переменная типа x (x является абстрактным классом) не установлена в тесте. этот тестовый класс не добавляется в набор тестов, но в его подклассы, которые имеют метод установки, устанавливающий переменную для конкретной реализации x. таким образом, я не дублирую тестовый код. подклассы неиспользуемого теста могут добавлять дополнительные методы теста, если это необходимо.</p>

<p>если ваш абстрактный класс содержит конкретные функциональные возможности, имеющие деловую ценность, я обычно проверяю его напрямую, создавая двойной тест, который заглушает абстрактные данные, или использую фальшивую среду, чтобы сделать это для меня. какой из них я выберу, во многом зависит от того, нужно ли мне писать специфичные для теста реализации абстрактных методов или нет.</p><p>наиболее распространенный сценарий, в котором мне нужно это сделать, - это когда я использую <a href="http://en.wikipedia.org/wiki/template\_method\_pattern">шаблон шаблона</a> , например, когда я создаю какой-то расширяемый фреймворк, который будет использоваться третьей стороной. в этом случае абстрактный класс - это то, что определяет алгоритм, который я хочу протестировать, поэтому имеет смысл проверить абстрактную базу, а не конкретную реализацию.</p><p>однако я думаю, что важно, чтобы эти тесты были сфокусированы только на <strong> конкретных реализациях реальной бизнес-логики </strong>; вы не должны юнит-тестировать <em> детали реализации </em> абстрактного класса, потому что в итоге вы получите хрупкие тесты.</p>

<p>чтобы выполнить модульный тест специально для абстрактного класса, вы должны получить его для целей тестирования, проверки результатов base.method () и предполагаемого поведения при наследовании.</p><p>вы тестируете метод, вызывая его, поэтому тестируйте абстрактный класс, реализуя его ...</p>

<p>один из способов - написать абстрактный контрольный пример, соответствующий вашему абстрактному классу, а затем написать конкретные контрольные примеры, которые подклассируют ваш абстрактный контрольный пример. сделайте это для каждого конкретного подкласса вашего исходного абстрактного класса (т.е. ваша иерархия тестовых примеров отражает вашу иерархию классов). см. проверка интерфейса в книге получателей junit: <a href="http://safari.informit.com/9781932394238/ch02lev1sec6">http://safari.informit.com/9781932394238/ch02lev1sec6</a> .</p><p>см. также суперкласс testcase в шаблонах xunit: <a href="http://xunitpatterns.com/testcase%20superclass.html">http://xunitpatterns.com/testcase%20superclass.html</a> </p>

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

<p>это шаблон, который я обычно использую при настройке привязки для тестирования абстрактного класса:</p><pre><code>public abstract class mybase{ /*...*/ public abstract void voidmethod(object param1); public abstract object methodwithreturn(object param1); /*,,,*/ } </code></pre><p>и версия, которую я использую в тесте:</p><pre><code>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); } /*,,,*/ } </code></pre><p>если абстрактные методы вызываются, когда я этого не ожидаю, тесты не пройдены. при организации тестов я легко могу заглушить абстрактные методы лямбдами, которые выполняют утверждения, генерируют исключения, возвращают различные значения и т.д.</p>

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

<p>одним из основных мотивов использования абстрактного класса является включение полиморфизма в вашем приложении, т.е. вы можете заменить другую версию во время выполнения. фактически, это почти то же самое, что и использование интерфейса, за исключением того, что абстрактный класс обеспечивает некоторые общие условия, часто называемые <em> шаблоном шаблона </em>.</p><p>с точки зрения модульного тестирования необходимо учитывать две вещи:</p><ol> <li> <p><strong>взаимодействие вашего абстрактного класса с соответствующими классами</strong>. использование модели фиктивного тестирования идеально подходит для этого сценария, поскольку оно показывает, что ваш абстрактный класс хорошо сочетается с другими.</p> </li> <li> <p><strong>функциональность производных классов</strong>. если у вас есть пользовательская логика, которую вы написали для своих производных классов, вам следует тестировать эти классы изолированно.</p> </li> </ol><p>правка: rhinomocks - это отличная среда для фиктивного тестирования, которая может генерировать фиктивные объекты во время выполнения, динамически наследуя ваш класс. такой подход может сэкономить вам бесчисленные часы ручного кодирования производных классов.</p>

<p>во-первых, если абстрактный класс содержит какой-то конкретный метод, я думаю, вы должны сделать это, рассмотрев этот пример.</p><pre><code> 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. } } </code></pre>

<p>я полагаю, вы могли бы захотеть протестировать базовую функциональность абстрактного класса ... но вам, вероятно, лучше всего расширить класс без переопределения каких-либо методов и сделать насмешку с минимальными усилиями для абстрактных методов.</p>

<p>после ответа @ patrick-desjardins я реализовал абстрактный и его класс реализации вместе с <code>@test</code> следующим образом:</p><p><strong>абстрактный класс - abc.java</strong></p><pre><code>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; } } </code></pre><p>как <strong><em> абстрактные классы не могут быть созданы, но они могут быть разделены на подклассы </em></strong>, конкретный класс <strong>def.java</strong> , как следует:</p><pre><code>public class def extends abc { @override public string sayhello() { return "hello!"; } } </code></pre><p><strong>@ test</strong> класс для проверки как абстрактного, так и неабстрактного метода:</p><pre><code>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")); } } </code></pre>

<p>если абстрактный класс подходит для вашей реализации, протестируйте (как предложено выше) производный конкретный класс. ваши предположения верны.</p><p>чтобы избежать путаницы в будущем, имейте в виду, что этот конкретный тестовый класс - не фиктивный, а <strong>поддельный</strong>.</p><p>строго говоря, <strong>mock</strong> определяется следующими характеристиками:</p><ul> <li>макет используется вместо каждой зависимости тестируемого предметного класса.</li> <li>макет - это псевдо-реализация интерфейса (вы можете вспомнить, что, как правило, зависимости должны быть объявлены как интерфейсы; тестируемость является одной из основных причин этого)</li> <li>поведение членов интерфейса макета - будь то методы или свойства - предоставляется во время тестирования (опять же, с использованием фальшивой среды). таким образом, вы избегаете связи тестируемой реализации с реализацией ее зависимостей (которые должны иметь свои собственные дискретные тесты).</li> </ul>