Comments 25
Извиняюсь, если вопрос покажется глупым, а зачем тестировать приватные методы? Тестироваться должна конечная логика класса. А приватные методы это подробности реализации, проведение рефакторинга реализации не должно ломать никакие тесты и не должно провоцировать написание новых, если логика не меняется. Иначе смысл приватности полностью теряется.
Или это связано с тем, что в PHP нет friendly-видимости?
Что означает "friendly видимость"? Тестирование приватных методов не имеет смысла, согласен. Нужно строить свой код так, чтобы входные параметры попадали на все условия приватных методов. Все будет ок, больше DI, инверсий зависимости, разнесения на слои и прочее.
Friendly-видимость, это когда члены класса видны дружественным модулям/классам. Дружественными модулями/классами, обычно, объявляются юнит-тесты. Таким образом, легально определяются полу-публичный член, специально для целей тестирования.
Другим способом тестировать частично закрытую логику, это protected-методы. Для тестирования достаточно сделать наследника, чтобы получить легальный доступ к тестируемому методу. Ведь protected-члены являются контрактом класса, так что тестировать protected-методы вполне валидно.
Но тестировать напрямую приватные члены как по мне является нарушением гигиены кода. Такая потребность говорит о плохом дизайне класса, раз приходится лезть в его кишки. Собственно из этого исходит основная претензия к библиотекам, упрощающим и легализующим это безобразие :)
Немного внесу ясность. Если исследовать современные PHP компоненты, то вы увидите что в классах в основном приватные и публичные свойства/методы. И зачастую финализированы . Программисты уходят от расширения по иерархии и предпочитают расширяться горизонтально через интерфейсы соблюдая принципы SOLID. Все что публичное - это официальное API и его нельзя изменять. Все что приватное, это основной алгоритм кода, который может меняться с каждой версией как угодно, но сохраняя поведение API.
ТО что вы предлагаете ,то для этого достаточно писать TDD тесты при которых вы можете реализовать открытое API.
Так вот когда вы примите это за идею, в юнит тестах вы столкнётесь с тем что у вас будет реализовано минимум логики в публичных методах и максимум логики в приватных методах. И чтобы сохранять ясность в поведении приватных методов которые вы пишите, вам приходится писать тесты для приватных методов. Так вы потихоньку погружаетесь в BDD тестирование при написании модулей.
Да и в конце концов это удобно. Когда вы пишите приватный метод и тут же для него пишите проверку. И благодаря такому тесту вы можете понимать что поломалось при внесении расширений кода или его изменений.
Также вы должны понимать что в `final class` не возможно провести наследования для создания фикстуры теста. А значит простым наследованием вы не отделаетесь.
Final class означает что программист не хочет расширять класс и задумываться о поддержке зависимых компонентов.
Следующее преимущество вторжения в приватную зону, это замена зависимостей.
Если зависимости объекта хранятся в приватных свойствах, то тем самым с помощью вторжения вы можете создавать "пустышки" которые симулируют поведение зависимостей. Для чего вам это надо? Например объект работает с записями БД. А вы не хотите чтобы тестовые записи попадали в бд. И вам нужно симулировать поведение базы данных в поведении класса (это я привел для примера).
НУ а для убедительности посмотрите на компонент spatie/invade - почти 2.8M скачиваний , 267 звезд, 72 зависимости. Значит есть потребность во вторжении в приватную зону.
Вот это меня и откровенно удивляет, что есть потребность вторжения в приватную зону. Все зависимости по идее должны внедряться через конструктор, следуя принципам SOLID. Зависимости должны быть абстракциями, следуя тем же принципам. Запрет тестировать приватные методы приведёт только к повышению качества кода, развяжет разработчикам руки.
Разработчик должен быть свободен проводить любой рефакторинг реализации, следуя TDD, не меняя тесты. Если же, например, разбив один приватный метод на два, разработчику придётся идти и переписывать тесты, это к TDD не имеет никакого отношения, уж извините.
Да и в конце концов это удобно. Когда вы пишите приватный метод и тут же для него пишите проверку.
Боюсь показаться не вежливым, навязывая своё мнение, но всё же скажу. TDD это про тестирование конечной логики модуля. Важно реализует ли он правильно логику, а сколько там приватных методов, 1 или 10, не должно иметь значения для тестов, иначе тесты превращаются в какую-то беспощадную самоцель (тесты ради тестов), и приводят просто к лишней работе, а разработчик даже и рефакторингом зачастую не станет заниматься, так как ему ещё в довесок надо переписать тесты, хотя логика даже не менялась.
Спорить со звёздами на гитхабе это конечно такое себе :) Но всё же на мой взгляд выглядит это совсем не хорошо: тестирование приватных методов, учитывая, что это делается вопреки правилам языка, нарушая границы видимости, используя при этом различные хаки, да и не соответствует это ни TDD, ни SOLID-у.
И немного ещё в кассу. Как связано тестирование приватных методов и BDD, этого я совсем не понял. Очевидно, что тестирование приватных методов явно противоречит BDD и по форме и по содержанию.
Если зависимости объекта хранятся в приватных свойствах, то тем самым с помощью вторжения вы можете создавать "пустышки" которые симулируют поведение зависимостей.
Инъекция зависимостей не применяется что ли? Откуда зависимости в приватных свойствах появляются-то?
BDD это случай когда тест описывает поведение проектируемого модуля. При этом на практике можно столкнуться с частным случаем оценки этого поведения (когда поведение сокрыто от внешней среды, но ему нужно дать анализ поведения).
Инъекция зависимостей не применяется что ли? Откуда зависимости в приватных свойствах появляются-то?
Случаи разные бывают. Порой сталкиваешься с таким что не до интерфейсов и внедрения зависимостей.
Ок. Возможно вы профессионал и у вас больше опыта. Я как любитель стремлюсь покрывать больше кода. Часто я переписываю или дописываю код , который забросил больше года назад, и только благодаря покрытию кода тестами я могу знать какую логику я заложил в код.
Любое внедрение зависимостей это уже есть Агрегация.
Например есть частный случай .
К примеру у вас есть Композиция. Нужно отрефакторить так, чтобы можно было заменить зависимость для тестирования, при этом класс должен сохранять паттерн Композиция. Дополнять класс публичными методами просто так запрещено (Любое объявление публичных методов - это официальное API ).
Также финализацию класса нужно сохранить ибо класс только для использования , а не для расширения.
<?php
class Dep{}
interface FactoryInterface {
public function run();
}
funal class Factory implements FactoryInterface
{
private \Dep $dep;
public function __construct ()
{
$this->dep= new Dep();
}
public function run ()
{
/....
}
}
Решение конечно есть моему примеру. Но ради проведение тестов вы вносите в код больше логики, ну которая по существу не нужна.
<?php
interface DepInterface{}
interface FactoryInterface {
public function run();
}
class Dep{}
abstract class FactoryAbstract implements FactoryInterface
{
protected \DepInterface $dep;
public function run ()
{
/....
}
}
final class Factory extends FactoryAbstract
{
public function __construct ()
{
$this->dep= new Dep();
}
}
Хотя при тестировании вы можете просто заменить инъекцию
`$sucker->set('dep',new EmptyDep());`
Что-то у вас куча несостыковок получается. Давайте рассмотрим их детально.
Случаи разные бывают. Порой сталкиваешься с таким что не до интерфейсов и внедрения зависимостей.
Получается хватает времени на то, чтобы пойти на обман языка, и с помощью всяких хаков повесить тесты на приватные методы, вместо того, чтобы потратить время и написать хороший код.
Часто я переписываю или дописываю код , который забросил больше года назад, и только благодаря покрытию кода тестами я могу знать какую логику я заложил в код.
Хорошо сказано! Юнит-тесты не только тестируют логику, но ещё и являются документацией к использованию компонентов, показывают как класс может использоваться и какие инварианты обрабатываются.
Но что у нас получается? Допустим, класс содержит метод run()
. Заходим в тесты, и видим тестирование каких-то doing_some()
, working_with_another()
и ещё кучу каких-то методов, которые тестируются, а в контракте их и близко нет.
Чем это мне поможет, как потребителю класса? Ничем. Придётся разбираться во всех внутренностях класса, ведь тесты мне не помогают разобраться с использованием. Они делают что-то другое.
Очередной обман.
Нужно отрефакторить так, чтобы можно было заменить зависимость для тестирования, при этом класс должен сохранять паттерн Композиция.
Вы что-то путаете. Композиция это как раз про наследование, и про иерархию объектов в компоновщике. Класс не может "сохранять паттерн", это паттерн для мира ООП, который обычно используется обычно для построения UI.
Для тестирования же ключевым является возможность подстановки своей заглушки для зависимости (mock/stub). Используем абстракции и внедрение зависимостей через конструктор = получаем возможность мокать что угодно совершенно легально.
К этому же получаем хороший качественный код. Правильное тестированое провоцирует писать хороший код!
Дополнять класс публичными методами просто так запрещено (Любое объявление публичных методов - это официальное API ).
Если у вас есть интерфейс, который все используют, и есть реализация этого интерфейса — класс, который никто напрямую не использует, то вы можете дополнять свой класс любыми методами. Какими хотите. Потому что, добавленные методы никак не влияют на интерфейс и не влияют на API.
Также финализацию класса нужно сохранить ибо класс только для использования , а не для расширения.
Если у разработчика нет каких-то прям явных и очевидных причин финализировать класс, этого не надо делать. Тем более, если класс это реализация интерфейса, а напрямую его никто не использует, только через инъекцию. Маниакальное упорство везде втыкать final
ни чем хорошим не вознаграждается.
И вот же какая несостыковка.
Значит, мы вставляем final
, чтобы явно что-то запретить, чего запрещать и смысла не имеет. Вставляем private
, чтобы точно также явно что-то запретить, но при этом целенаправленно это нарушаем! Просто плюём на правила языка.
По мне, так это какой-то тотальный самообман, борьба с ветряными мельницами.
Ещё немного ИМХО про final
. Это где-то в библии PHP написано, что все его втыкают везде, где не попадя? :)
Почему бы не оставить класс расширяемым, для тех же тестов?
При активном использовании интерфейсов, все потребители сидят на интерфейсах, всем до лампочки зафиналена реализация или нет.
Почему? Потому что это не работает. Раз у нас есть интерфейс, значит мы можем при желании задекорировать зафиналенный класс, и легчайше обойти его final
, предоставив свою реализацию интерфейса. При чём без всяких хаков, вполне себе легально.
Ну и стоило ли оно того? Я считаю нет.
Подумайте ещё вот о чём.
Если бы разработчки языка решили, что любой класс должен быть финальным кроме тех, которые явно планируется расширять, то вместо final
добавили бы ключевое слово extendable
, и по умолчанию всё было бы финальным :)
По длительному опыту разработки, сталкивался и довольно часто с зафиналеным классом от разработчика и это доставляло мне кучу совершенно не нужных мне проблем.
Хотя при тестировании вы можете просто заменить инъекцию
$sucker->set('dep',new EmptyDep());
Если зависимости правильно внедряются через конструктор, то заменить можно без всякого sucker-а, без всяких хаков и приколов :)
А ваши тесты будут полностью раскрывать использование класса, без каких-то левых приватных методов.
А если член dep
переименуется или удалиться, то IDE даже не сможет указать на это, и никакой стат анализ этого не увидит. В компилируемых же языках, это вообще крайне плохая и порицаемая практика, так как скомпилируется очевидно неправильный код. В общем плохо абсолютно всё, а преимуществ не видно.
Композиция это не про иерархию. Есть понятия в ООП - наследование через композицию и агрегацию, вместо иеархии.
Возможно у вас другие представления об Композиции но у меня именно такие как описал ниже.
Из просторов интернета.
Наследование через Композицию
<?php
class A {
public function helloWorld() {
echo 'Hello, World!';
}
}
class B {
protected A $a;
public function __construct() {
$this->a = new A(); // создает объект другого класса
}
public function sayHello() {
$this->a->helloWorld(); // использует объект другого класса
}
}
$obj = new B();
$obj->sayHello(); // Hello, World!
Наследование через Агрегацию
<?php
class A {
public function helloWorld() {
echo 'Hello, World!';
}
}
class B {
protected A $a;
public function __construct(A $a) {
$this->a = $a;
}
public function sayHello() {
$this->a->helloWorld(); // использует объект другого класса
}
}
$objA = new A(); // создает объект другого класса
$objB = new B($objA);
$objB->sayHello(); // Hello, World!
Данные способы вы можете применять как для для наследования, так и для организации зависимостей класса.
Не знаю откуда уж вы черпаете информацию, но вот вам Composite pattern. Это паттерн.
Из просторов интернета. Наследование через Композицию
То, что вы привели, это мог бы быть чистый Adapter pattern, если бы вы реализовывали какой-то интерфейс.
А так, это вообще никакой не паттерн и к наследованию не имеет абсолютно никакого отношения.
У вас есть классы A, и B — совершенно отдельные классы, с разными методами. Класс B использует экземпляр класса A, чтобы реализовать собственную функцию. О каком наследовании здесь вообще идёт речь? Здесь ничего даже близко не наследуется.
Касательно наследования: ни в одном из двух ваших примеров, я не могу передать экземпляр класса B, в метод, который ждёт A.
Но я наверное от вас отстану с этим. Спасибо!
Вы так рассуждаете как будто ваши знанияи истинны.
Признаю, я подобрал не правильное выражение.
Я назвал Патетерн композиции. Надо было выразится яснее - Принципы Композиции или Агрегации.
Понятия Design Paterns было введено рядом авторов в 1994 году, которые описали ряд методик построения кода . А до этого прогеры были просто в недоумении что существуют паттерны (сарказм). Назовите из как угодно петтерны, шаблоны, методологии, постулаты, концепции. Сути этого не меняет. То что уже описано в концепции ООП, вы уже не найдете в тех списках популярных паттернов, потому что незачем изобретать велосипед.
И принцип композиции и агрегации зародились за долго до ООП. И также существуют в функциональном программировании. Даже в математике есть такие определения.
И в примерах я вам эти принципы в рамках ООП описал в простоя и ясном коде.
Композиция - это монолит или не разборный компонент. Любой механик вам это подтвердит.
А Агрегация - это агрегат состоящий из составных частей которые могут взаимозаменяться.
Термины даже взяты из жизни.
Но если надо я могу вам накидать ссылок , чтобы мои знания не поддавались сомнению.
https://habr.com/ru/articles/354046/
https://en.wikipedia.org/wiki/Object-oriented_programming#Composition,_inheritance,_and_delegation
https://en.wikipedia.org/wiki/Object_composition
https://en.wikipedia.org/wiki/Composition_over_inheritance
https://mohasin-dev.medium.com/object-composition-in-php-with-example-ce5855b0473b#:~:text=Object composition in PHP%2C as,combining or composing simpler objects.
В конце концов загуглите composition principle OOP и вы увидите массу ссылок с примерами. А потом говорите что у меня знания какие то не такие.
То что вы говорили про Composite pattern. - на русском этот паттерн называется как компоновщик (скорей всего чтобы не путаться в терминах).
Я вас неправильно понял, потому что вы сказали паттерн.
Но опять же. Имея огромный опыт разработки в чисто ООП-шных языках, я ни разу не наблюдал такой упоённой борьбы с наследованием, и агрессивным употребления финализации иерархии, как в PHP. Также особо нет необходимости бороться с наследованием через агрегацию. Это бессмысленная борьба, так как большинство задач решаются либо наследованием либо агрегацией вполне естественным и очевидным образом, без размышлений "а это наследование? а это композиция? а это агрегация?" -- есть принципы SOLID, DRY, KISS, если вы их соблюдаете, у вас хороший код. Не надо сидеть задумчиво над кодом и страдать на тему, а надо ли тут наследование или лучше агрегацию... Такой дилеммы не должно быть в принципе :) И то и другое -- прекрасные инструменты, хорошо решающие свой класс задач.
Возвращаясь к теме, не важно сколько там у кого звёзд на гитхабе, скачиваний и т.п. Тестирование приватных методов совершенно явно противоречит ООП, провоцирует писать плохой код, замораживает реализацию. Да, это моё мнение, но оно основано на опыте.
Будет неправильно вас в чём-то убеждать, у вас своё мнение, у меня своё, это нормально. Но кому-то другому возможно будет полезным задуматься, и учиться на чужих ошибках, не наступая на одни и те же грабли.
Думаю вам просто нужно на финализацию посмотреть с другого ракурса. Ее как раз введи в PHP чтобы уйти от иерархии и связности и поддержать лучшие практики в SOLID . Тот же принцип open/closed раньше строился на иерархии. А сейчас на полиморфизме. И правило перекочевало на расширение абстрактных классов и интерфейсов , а не классов реализации. Если уходить от иеархий, код становится наиболее производительным,а смысловая нагрузка уменьшается.
По поводу теста приватных случаев.
Повторюсь - это на усмотрение программиста.
Есть тесты которые оценивают работоспособность API и при настройке тестовой среды будут запускаться только они.
А есть тесты которые содействуют в разработке И это частный случай. И хорошие помощники чтобы понимать что твои приватные методы работают правильно. Если вы как руководитель скажете что они не нужны в репо, то эти тесты будут храниться у прогера, а в репо тесты по феншую команды.
По поводу публичных и приватных методов.
что protected что private это зона привата.
в горизонтальном масштабировании protected теряет смысл.
Для меня если я сделал метод публичным это значит я подписал контракт что он где то будет использоваться и я не имею права как программист в дальнейшем вносить в его изменения. А с приватными методами это правило не работает. Я могу полностью переписать класс, но публичные методы должны сохранять поведение, и тесты публичных методов должны давать оценку сохранности поведения. Чем больше публичных методов, тем больше у программиста связаны руки. А если еще на них еще и интерфейсы наложены, то уже по сути вместо рефакторинга нужно писать замену этим классам.
Про финализацию скину вам статью
https://habr.com/ru/articles/482154/
Про PHPUnit. Если вы хоть раз залазили под капот модуля, то узнали бы что mock -и это создание классов через eval ('class A{...}') который наследует тестируемый класс, методы беспощадно копируются в тестируемый класс, и внедряются методы тестирования. За счет этого вы и можете переопределять методы в классе. Если для вас это чистое решение, тогда не понимаю почему через функционал PHP вторгаться в частную зону класса для проведения тестов это плохо.
Ну а поводу покрытия кода тестами https://ru.wikipedia.org/wiki/Покрытие_кода.
Вы можете разделить тесты на тесты публичных методов которые отражают способы использования и поведения API , и тогда ваши коллеги могут оценить как работает модуль, и на тесты которые хардкорно оценивают поведение модуля и в случае проблем указывают что не так.
В остальном я не хочу вступать в дискус ибо это уже субъективное мнение каждого.
Если зависимости правильно внедряются через конструктор, то заменить можно без всякого sucker-а, без всяких хаков и приколов :)
А ваши тесты будут полностью раскрывать использование класса, без каких-то левых приватных методов.А если член
dep
переименуется или удалиться, то IDE даже не сможет указать на это, и никакой стат анализ этого не увидит. В компилируемых же языках, это вообще крайне плохая и порицаемая практика, так как скомпилируется очевидно неправильный код. В общем плохо абсолютно всё, а преимуществ не видно.
если вы говорите про динамачиское удаление свойства в объектах,то они не удаляются, а переходят в состояние undefined при этом isset($obj-->prop)===false , а property_exist()===true. при вызове такого свойства выбросит ошибку. Для статических свойств при удалении выбросит ошибку. Поэтому не переживайте за IDE, это все происходит программно, а не вносит коррективы в код.
Если вы говорили про удаление dep в рамках рефакторинга кода, то я не улавливаю вашу мысль.
Не везде хорошо организовывать слабую связность. Слабая связность полезна при взаимодействии разных компонентов между собой и возможности их замены. Если вы реализуете компонент который состоит из десятка классов , но для взаимодействия предназначен только один или два , то не имеет смысла для всех классов формировать интерфейсы чтобы через них внедрять зависимости. Это порождает загрузку излишних классов/интерфейсов. Проще абстрагировать эти классы от внешней среды. И тогда появляется право корректировать эти классы или менять их дизайн (когда клас используется в рамках компонента и не предназначен для внешнего использования). Если реализован мост(интерфейс) для этого класса, то такое право пропадает. Если вы начинаете внедрять зависимости через конструктор (агрегация), а не порождать в конструкторе(композиция) , то такой класс начинает зависеть от внешней среды и создает связи. Композитный класс сам по себе заявляет - не вмешивайтесь в меня и не расширяйте меня, я выполняю строгий функционал, я не устанавливаю связи, все мои связи во мне.
Поэтому вы сталкиваетесь с рядом ограничений если хотите использовать такой класс как то по своему. Хотя программист который так написал , заранее предостерегая от шалавливых рук в будущем отсекая все связи. То что кто то жалуется что не по феншую - значит ему не стоит туда влазить.
И финализация от части предназначена чтобы убрать технический долг перед наследниками. Но как правило люди пишут финальные классы и интерфейсы, но забывают про трейты в целях позволить остальным горизонтальное расширение. Если для вас реализован мост ( через интерфейс), то пишите свой компонент заимствуйте трейты, если есть. Если нет моста, то не стоит даже этот финальный класс как то расширять. Значит разработчик компонента не захотел чтобы этот класс как то расширялся (наложил табу). Если решили расширить финальный класс, используйте наследование через композицию. Или как вы сказали - паттерн адаптер.
И согласитесь если вы меняете код ради тестов, то уже что то делаете не так. Согласен код должен быть тестируемым но не в разрез концепций разработки (например когда заставляете разработчиков отказываться от финальных классов. Хотя попросту надо было разобраться в теме горизонтального расширения).
Также я заметил для вас внедрение зависимостей это панацея. Повторюсь все хорошо если в меру. Отсутствие такого поведения это не означает что код плохо спроектирован. Просто кодом это поведение не предусмотрено. Возможно программист написавший код знает что то больше, чего не знаете вы. Тогда найдите другие пути тестирования и подмены зависимостей.
Интересно, у нас в компании недавно как раз были дебаты по поводу тестирования приватных методов. В основном решили что:
Приватные методы не тестируем
Если все же есть такая необходимость, значит большая вероятность того что класс спроектирован неправильно и этот метод скорее всего должен быть вынесен в другой класс и сделан публичным
Смотря что реализует команда - модули или приложения. Также зависит от дизайна проектирования. Покрывать все и вся тесты это затратное время. Поэтому зачастую покрывают только публичные методы. Покрытие тестами приватных методов - это частный случай программиста. Например у вас есть один публичный метод run() в рамках интерфейса, а вся логика реализована в приватных методах и они между собой как то взаимодействуют. И чтобы предсказать поведение вам приходится писать тесты для приватных методов. Но это что касается написание модулей. Для написание приложений там другая концепция проектирования тестов
Интересно, а как это внутри приложения всего 1 публичный метод? Как классы между собой взаимодействуют?
Мы же тесты пишем внутри приложения
Легко. Например "interface Runnable{ public function run();}" говорит о том что ему достаточно реализации одного публичного метода run. Если вы пишите реализацию этого интерфейса, то делать остальные методы публичными не имеет смысла.
Публичный метод это API класса. Для этого в ооп и придумали инкапсуляцию.
Классы должны взаимодействовать через интерфейсы.
Расширяя публичную часть вы уже не имеете права к ее изменению. А лишние публичные методы это уже как мусор.
Вы можете оъявить protected методы, но если `final class`, то хоть protected хоть private, не имеет значения.
Это какая-то маленькая программа, в которой не очень много бизнес-логики.
Классы взаимодействуют через интерфейсы, если они взаимодействуют, это уже значит что у каждого есть публичный метод. Если конечно это не наследование.
Это какая-то маленькая программа, в которой не очень много бизнес-логики.
А это уже принцип разделения Интерфейса в SOLID
https://ru.wikipedia.org/wiki/Принцип_разделения_интерфейса
а вся логика реализована в приватных методах и они между собой как то взаимодействуют.
В общем случае, эта логика просто выносится в некий сервис и вы тестируете уже его. А класс с методом run() тестируете на запуск этого сервиса с нужными параметрами.
Да, согласен. Обычно так и делаем. Но как правило если это не сферический конь в вакууме, то таких сервисов в программе десятки и даже в пределах одного домена может быть несколько сервисов одного уровня
если эта логика не удовлетворяет правилу единственной ответственности, то так и делается. А если вы непосредственную связную логику выносите в отдельный сервис, то вы создаете излишние связи и плодите зависимости которые нужно поддерживать.
Сейчас просто плодятся не нужные споры - а нужно ли тестировать приватные методы?
А придет к вам клиент с какой нибудь госкорпорации и скажет - хочу 100% покрытие кода, плачу хорошие деньги. И вопрос само собой отпадет.
Сейчас просто устоялась практика теста только публичной части ибо код порой бесконтрольно плодится и покрытие все и вся тестами это очень затратно. Согласно методикам прошлого века к тестам намного скурпулезней относились.
Sucker (присоска) — PHP компонент для теста приватных методов и свойств