В рамках хобби пишу свои собственные компоненты.
Но есть проблема - отсутствие аудитории.
Чтобы полноценно тестировать компоненты на работоспособность, решил через Хабр дать популярность некоторым своим компонентам.

На днях дописал компонент который универсально может взаимодействовать с приватными свойствами и методами классов и объектов. Цель такого доступа - тестировать работоспособность скрытого кода.
alpa/tools_sucker - https://packagist.org/packages/alpa/tools_sucker
https://github.com/alexeyp0708/php_tools_sucker

Возможно вы зададитесь вопросом - тема стара как мир, зачем еще один схожий компонент?

Например есть легковесная в несколько строк spatie/invade - https://packagist.org/packages/spatie/invade

Да и "на коленке" можно написать свою реализацию.

НО..., согласитесь, всегда охота иметь под рукой легко понятный, прозрачный и универсальный инструмент.

Но хз. Мне нравится писать свои тяжеловесные компоненты.
Ознакомившись, вы поймете насколько прост и эффективен данный компонент.
Я дам обзор кратко. Если проявится интерес то обязательно оцените на гитхабе.

Установка и классы взаимодействия

Установка

composer require alpa/tools_sucker:1.0.*

Использование

Есть три рабочих класса взаимодействия с приватными членами объекта/класса. Но для обзора я опишу два класса:

  • \Alpa\Tools\Sucker\Sucker - использует явные геттеры и сеттеры для взаимодействия с объектом или классом.

  • Alpa\Tools\Sucker\Proxy - прокси объект, который использует магию для доступа к объектам и свойствам объекта или класса.

    Для примера создадим два подопытных класса

<?php 

class A{
    private $private_prop='hello';
    private static $static_private_prop='hello';
    private function private_method($arg)
    {
        return $arg;
    }
    private function & private_methodByReference(&$arg=null)
    {
        return $arg;
    }
}

class B extends A{
    private $private_prop='bay';
    private static $static_private_prop='bay';
    private function private_method($arg)
    {
        return strtoupper($arg);
    }
}

Sucker класс

Доступ к приватным членам объекта

<?php

use A;
use B;
use Alpa\Tools\Sucker\Sucker;

//  доступ к приватным членам  обьекта. 
$sucker=new Sucker(new B);
// получаем доступ к приватному совйтсву класса B
echo $sucker->get('private_prop');// bay
echo "\n";
// получаем доступ к приватному совйтсву родительского класса A
echo $sucker(A::class)->get('private_prop');// hello  
echo "\n";
echo $sucker->get('private_prop');// bay
echo "\n";

//get by reference
$var = & $sucker(A::class)->get('private_prop');// $var = & A::$private_prop ==='hello'

Доступ к приватным статическим членам класса

<?php

use A;
use B;
use Alpa\Tools\Sucker\Sucker;

//  доступ к приватным  статическим членам  класса. 
$sucker=new Sucker(B::class);
echo $sucker->get('static_private_prop');// bay
echo "\n";
echo $sucker(A::class)->get('static_private_prop');// hello  
echo "\n";
// Warn : The Scope is automatically reset after calling methods that are responsible for accessing members of the observable object
echo $sucker->get('static_private_prop');// bay
echo "\n";

//get by reference
$var = & $sucker(A::class)->get('static_private_prop');// $var = & A::$static_private_prop ==='hello'

API Sгcker класса

<?php

use Alpa\Tools\Sucker\Sucker;

$sucker = new Socker($object); // for members object
// or
$sucker=new Sucker(Target::class); // for static members class

//  при смене области видимости, обьект один и тот же (а не создается новый)
$sucker===$sucker(A::class);  
// сброc область видимости
$sucker(null); 
/* Внимание: область видимости сохраняется до первого вызова метода API 
и после сбрасывается на область видимости по умолчанию (класс обьекта).*/

/* & - указывает что аргументы нужно передавать по ссылке (в аргументах указан только для наглядности), 
или можно получать результат по ссылке. */
// запросить значение  свойства обькта/класса (можно по сыслке)
& $sucker->get( $prop ); & $sucker( SCOPE::class )->get( $prop );

// установить значение для свойства обькта/класса 
$sucker->set( $prop,  $value ); $sucker(SCOPE::class)->set( $prop, $value );

// установить значение для свойства обькта/класса по ссылке
$sucker->setRef( $prop, & $value ); $socker(SCOPE::class)->setRef( $prop, & $value );

// проверить наличие свойства обькта/класса 
$sucker->isset( $prop ); $sucker(SCOPE::class)->isset( $prop );

// удалить свойство из обьекта. для статического свойства класса будет вызвана стандартная ошибка. 
$sucker->unset( $prop ); $sucker(SCOPE::class)->unset( $prop );

// перебрать свойства объекта/класса.
/* in each closure  => self::class===SCOPE::class and opening $this */
$sucker->each(function ( $key, & $value ){return true;/* break*/}); $sucker( SCOPE::class )->each( function ($key, & $value){return true;/*break*/} );

// вызвать метод объекта/класса.
& $sucker->call( $method, ...$args ); & $sucker( SCOPE::class )->call( $prop ,...$args );

// вызвать метод объекта/класса с возможностьбю передачи аргументов по ссылке.
& $sucker->apply( $method, [& $arg,...] ); & $sucker( SCOPE::class )->apply( $method, [& $arg,...] ); 

// Песочница для обработки обьекта по своему усмотрению
/* in sandbox closure  => self::class===SCOPE::class and opening $this */
& $sucker->sandbox( function & (& $arg){},[ & $arg,...] ); & $sucker( SCOPE::class )->sandbox( function & (& $arg){},[ & $arg,...] );
 

Proxy класс

Объект прокси класса позволяет работать, так как будто вы имеете контакт с самим объектом.

<?php

use A;
use B;
use Alpa\Tools\Sucker\Proxy;

$proxy=new Proxy(new B);
echo $proxy->private_prop;// bay
echo $proxy(A::class)->private_prop;// hello  
echo $proxy->private_prop;// bay

// доступ к статическим свойствам класса.
$proxy=new Proxy(B::class);
echo $proxy->static_private_prop;// bay
echo $proxy(A::class)->static_private_prop;// hello  
echo $proxy->static_private_prop;// bay
// get by reference
$var =  & $proxy->static_private_prop;

API Proxy класса

<?php

use Alpa\Tools\Sucker\Proxy;

$proxy = new Proxy($object); // for members object
// or
$proxy=new Sucker(Target::class); // for static members class

// при смене области видимости, обьект один и тот же (а не создается новый)
$proxy===$proxy(A::class);  
// сброc область видимости
$proxy(null); 
/* Внимание: область видимости сохраняется до первого вызова метода API 
и после сбрасывается на область видимости по умолчанию (класс обьекта).*/

// запросить значение  свойства обькта/класса (можно по сыслке)
& $proxy->prop; & $proxy(SCOPE::class)->prop;

//  установить значение для свойства обькта/класса 
$proxy->prop=$vslue;  $proxy(SCOPE::class)->prop=$value;

// проверить наличие свойства обькта/класса 
isset($proxy->prop); isset($proxy(SCOPE::class)->prop); 

// удалить свойство из обьекта. для статического свойства класса будет вызвана стандартная ошибка. 
unset($proxy->prop); unset($proxy(SCOPE::class)->prop); 

// перебрать свойства объекта/класса.
foreach ($proxy as $key=>$value){
  
}
foreach ($proxy(SCOPE::class) as $key=>$value){
  
}

// вызвать метод объекта/класса.
& $proxy->method(...$args);  & $proxy(SCOPE::class)->method(...$args);

/* Песочница для обработки обьекта по своему усмотрению.
Аргументы можно передавать по ссылке, а также результат получать по ссылке*.
/* in sandbox closure  => self::class===SCOPE::class and opening $this */
& $proxy(function & (...$args){},[& $arg,...]); & $proxy(SCOPE::class)(function & (...$args){},[ & $arg,...]);

Удобно? Практично? - решать вам.

Сильно не критикуйте. Писать код это вам не "хухры мухры".

Всем спасибо. Ставьте звездочку на гитхабе.

Комментарии (25)


  1. hVostt
    05.11.2023 00:37
    +8

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

    Или это связано с тем, что в PHP нет friendly-видимости?


    1. mepihin
      05.11.2023 00:37

      Что означает "friendly видимость"? Тестирование приватных методов не имеет смысла, согласен. Нужно строить свой код так, чтобы входные параметры попадали на все условия приватных методов. Все будет ок, больше DI, инверсий зависимости, разнесения на слои и прочее.


      1. hVostt
        05.11.2023 00:37
        +1

        Friendly-видимость, это когда члены класса видны дружественным модулям/классам. Дружественными модулями/классами, обычно, объявляются юнит-тесты. Таким образом, легально определяются полу-публичный член, специально для целей тестирования.

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

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


        1. AlexeyP0708 Автор
          05.11.2023 00:37

          Немного внесу ясность. Если исследовать современные PHP компоненты, то вы увидите что в классах в основном приватные и публичные свойства/методы. И зачастую финализированы . Программисты уходят от расширения по иерархии и предпочитают расширяться горизонтально через интерфейсы соблюдая принципы SOLID. Все что публичное - это официальное API и его нельзя изменять. Все что приватное, это основной алгоритм кода, который может меняться с каждой версией как угодно, но сохраняя поведение API.
          ТО что вы предлагаете ,то для этого достаточно писать TDD тесты при которых вы можете реализовать открытое API.
          Так вот когда вы примите это за идею, в юнит тестах вы столкнётесь с тем что у вас будет реализовано минимум логики в публичных методах и максимум логики в приватных методах. И чтобы сохранять ясность в поведении приватных методов которые вы пишите, вам приходится писать тесты для приватных методов. Так вы потихоньку погружаетесь в BDD тестирование при написании модулей.
          Да и в конце концов это удобно. Когда вы пишите приватный метод и тут же для него пишите проверку. И благодаря такому тесту вы можете понимать что поломалось при внесении расширений кода или его изменений.

          Также вы должны понимать что в `final class` не возможно провести наследования для создания фикстуры теста. А значит простым наследованием вы не отделаетесь.
          Final class означает что программист не хочет расширять класс и задумываться о поддержке зависимых компонентов.

          Следующее преимущество вторжения в приватную зону, это замена зависимостей.
          Если зависимости объекта хранятся в приватных свойствах, то тем самым с помощью вторжения вы можете создавать "пустышки" которые симулируют поведение зависимостей. Для чего вам это надо? Например объект работает с записями БД. А вы не хотите чтобы тестовые записи попадали в бд. И вам нужно симулировать поведение базы данных в поведении класса (это я привел для примера).

          НУ а для убедительности посмотрите на компонент spatie/invade - почти 2.8M скачиваний , 267 звезд, 72 зависимости. Значит есть потребность во вторжении в приватную зону.


          1. hVostt
            05.11.2023 00:37
            +1

            Вот это меня и откровенно удивляет, что есть потребность вторжения в приватную зону. Все зависимости по идее должны внедряться через конструктор, следуя принципам SOLID. Зависимости должны быть абстракциями, следуя тем же принципам. Запрет тестировать приватные методы приведёт только к повышению качества кода, развяжет разработчикам руки.

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

            Да и в конце концов это удобно. Когда вы пишите приватный метод и тут же для него пишите проверку.

            Боюсь показаться не вежливым, навязывая своё мнение, но всё же скажу. TDD это про тестирование конечной логики модуля. Важно реализует ли он правильно логику, а сколько там приватных методов, 1 или 10, не должно иметь значения для тестов, иначе тесты превращаются в какую-то беспощадную самоцель (тесты ради тестов), и приводят просто к лишней работе, а разработчик даже и рефакторингом зачастую не станет заниматься, так как ему ещё в довесок надо переписать тесты, хотя логика даже не менялась.

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

            И немного ещё в кассу. Как связано тестирование приватных методов и BDD, этого я совсем не понял. Очевидно, что тестирование приватных методов явно противоречит BDD и по форме и по содержанию.

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

            Инъекция зависимостей не применяется что ли? Откуда зависимости в приватных свойствах появляются-то?


            1. AlexeyP0708 Автор
              05.11.2023 00:37

              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 ()
                {
                /....
                }
              } 


              1. AlexeyP0708 Автор
                05.11.2023 00:37

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

                <?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());`


                1. hVostt
                  05.11.2023 00:37
                  +1

                  Что-то у вас куча несостыковок получается. Давайте рассмотрим их детально.

                  Случаи разные бывают. Порой сталкиваешься с таким что не до интерфейсов и внедрения зависимостей.

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

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

                  Хорошо сказано! Юнит-тесты не только тестируют логику, но ещё и являются документацией к использованию компонентов, показывают как класс может использоваться и какие инварианты обрабатываются.

                  Но что у нас получается? Допустим, класс содержит метод 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 даже не сможет указать на это, и никакой стат анализ этого не увидит. В компилируемых же языках, это вообще крайне плохая и порицаемая практика, так как скомпилируется очевидно неправильный код. В общем плохо абсолютно всё, а преимуществ не видно.


                  1. AlexeyP0708 Автор
                    05.11.2023 00:37

                    Композиция это не про иерархию. Есть понятия в ООП - наследование через композицию и агрегацию, вместо иеархии.
                    Возможно у вас другие представления об Композиции но у меня именно такие как описал ниже.

                    Из просторов интернета.
                    Наследование через Композицию

                    <?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!

                    Данные способы вы можете применять как для для наследования, так и для организации зависимостей класса.


                    1. hVostt
                      05.11.2023 00:37

                      Не знаю откуда уж вы черпаете информацию, но вот вам Composite pattern. Это паттерн.

                      Из просторов интернета. Наследование через Композицию

                      То, что вы привели, это мог бы быть чистый Adapter pattern, если бы вы реализовывали какой-то интерфейс.

                      А так, это вообще никакой не паттерн и к наследованию не имеет абсолютно никакого отношения.

                      У вас есть классы A, и B — совершенно отдельные классы, с разными методами. Класс B использует экземпляр класса A, чтобы реализовать собственную функцию. О каком наследовании здесь вообще идёт речь? Здесь ничего даже близко не наследуется.

                      Касательно наследования: ни в одном из двух ваших примеров, я не могу передать экземпляр класса B, в метод, который ждёт A.

                      Но я наверное от вас отстану с этим. Спасибо!


                      1. AlexeyP0708 Автор
                        05.11.2023 00:37

                        Вы так рассуждаете как будто ваши знанияи истинны.
                        Признаю, я подобрал не правильное выражение.
                        Я назвал Патетерн композиции. Надо было выразится яснее - Принципы Композиции или Агрегации.
                        Понятия 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. - на русском этот паттерн называется как компоновщик (скорей всего чтобы не путаться в терминах).


                      1. hVostt
                        05.11.2023 00:37

                        Я вас неправильно понял, потому что вы сказали паттерн.

                        Но опять же. Имея огромный опыт разработки в чисто ООП-шных языках, я ни разу не наблюдал такой упоённой борьбы с наследованием, и агрессивным употребления финализации иерархии, как в PHP. Также особо нет необходимости бороться с наследованием через агрегацию. Это бессмысленная борьба, так как большинство задач решаются либо наследованием либо агрегацией вполне естественным и очевидным образом, без размышлений "а это наследование? а это композиция? а это агрегация?" -- есть принципы SOLID, DRY, KISS, если вы их соблюдаете, у вас хороший код. Не надо сидеть задумчиво над кодом и страдать на тему, а надо ли тут наследование или лучше агрегацию... Такой дилеммы не должно быть в принципе :) И то и другое -- прекрасные инструменты, хорошо решающие свой класс задач.

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

                        Будет неправильно вас в чём-то убеждать, у вас своё мнение, у меня своё, это нормально. Но кому-то другому возможно будет полезным задуматься, и учиться на чужих ошибках, не наступая на одни и те же грабли.


                      1. AlexeyP0708 Автор
                        05.11.2023 00:37

                        Думаю вам просто нужно на финализацию посмотреть с другого ракурса. Ее как раз введи в PHP чтобы уйти от иерархии и связности и поддержать лучшие практики в SOLID . Тот же принцип open/closed раньше строился на иерархии. А сейчас на полиморфизме. И правило перекочевало на расширение абстрактных классов и интерфейсов , а не классов реализации. Если уходить от иеархий, код становится наиболее производительным,а смысловая нагрузка уменьшается.

                        По поводу теста приватных случаев.

                        Повторюсь - это на усмотрение программиста.

                        Есть тесты которые оценивают работоспособность API и при настройке тестовой среды будут запускаться только они.

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

                        По поводу публичных и приватных методов.

                        что protected что private это зона привата.

                        в горизонтальном масштабировании protected теряет смысл.

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


                  1. AlexeyP0708 Автор
                    05.11.2023 00:37

                    Про финализацию скину вам статью
                    https://habr.com/ru/articles/482154/
                    Про PHPUnit. Если вы хоть раз залазили под капот модуля, то узнали бы что mock -и это создание классов через eval ('class A{...}') который наследует тестируемый класс, методы беспощадно копируются в тестируемый класс, и внедряются методы тестирования. За счет этого вы и можете переопределять методы в классе. Если для вас это чистое решение, тогда не понимаю почему через функционал PHP вторгаться в частную зону класса для проведения тестов это плохо.

                    Ну а поводу покрытия кода тестами https://ru.wikipedia.org/wiki/Покрытие_кода.
                    Вы можете разделить тесты на тесты публичных методов которые отражают способы использования и поведения API , и тогда ваши коллеги могут оценить как работает модуль, и на тесты которые хардкорно оценивают поведение модуля и в случае проблем указывают что не так.

                    В остальном я не хочу вступать в дискус ибо это уже субъективное мнение каждого.


                  1. AlexeyP0708 Автор
                    05.11.2023 00:37

                    Если зависимости правильно внедряются через конструктор, то заменить можно без всякого sucker-а, без всяких хаков и приколов :)
                    А ваши тесты будут полностью раскрывать использование класса, без каких-то левых приватных методов.

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

                    если вы говорите про динамачиское удаление свойства в объектах,то они не удаляются, а переходят в состояние undefined при этом isset($obj-->prop)===false , а property_exist()===true. при вызове такого свойства выбросит ошибку. Для статических свойств при удалении выбросит ошибку. Поэтому не переживайте за IDE, это все происходит программно, а не вносит коррективы в код.

                    Если вы говорили про удаление dep в рамках рефакторинга кода, то я не улавливаю вашу мысль.

                    Не везде хорошо организовывать слабую связность. Слабая связность полезна при взаимодействии разных компонентов между собой и возможности их замены. Если вы реализуете компонент который состоит из десятка классов , но для взаимодействия предназначен только один или два , то не имеет смысла для всех классов формировать интерфейсы чтобы через них внедрять зависимости. Это порождает загрузку излишних классов/интерфейсов. Проще абстрагировать эти классы от внешней среды. И тогда  появляется право корректировать эти классы или менять их дизайн (когда клас используется в рамках компонента и не предназначен для внешнего использования).  Если реализован мост(интерфейс) для этого класса, то такое право пропадает. Если вы начинаете внедрять зависимости через конструктор (агрегация), а не порождать в конструкторе(композиция) , то такой класс начинает зависеть от внешней среды и создает связи.  Композитный класс сам по себе заявляет - не вмешивайтесь в меня и не расширяйте меня, я выполняю строгий функционал, я не устанавливаю связи, все мои связи во мне.

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

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

                    И согласитесь если вы меняете код ради тестов, то уже что то делаете не так. Согласен код должен быть тестируемым но не в разрез концепций разработки (например когда заставляете разработчиков отказываться от финальных классов. Хотя попросту надо было разобраться в теме горизонтального расширения).

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


  1. MyraJKee
    05.11.2023 00:37

    Интересно, у нас в компании недавно как раз были дебаты по поводу тестирования приватных методов. В основном решили что:

    1. Приватные методы не тестируем

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


    1. AlexeyP0708 Автор
      05.11.2023 00:37
      +1

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


      1. MyraJKee
        05.11.2023 00:37

        Интересно, а как это внутри приложения всего 1 публичный метод? Как классы между собой взаимодействуют?

        Мы же тесты пишем внутри приложения


        1. AlexeyP0708 Автор
          05.11.2023 00:37

          Легко. Например "interface Runnable{ public function run();}" говорит о том что ему достаточно реализации одного публичного метода run. Если вы пишите реализацию этого интерфейса, то делать остальные методы публичными не имеет смысла.

          Публичный метод это API класса. Для этого в ооп и придумали инкапсуляцию.

          Классы должны взаимодействовать через интерфейсы.

          Расширяя публичную часть вы уже не имеете права к ее изменению. А лишние публичные методы это уже как мусор.

          Вы можете оъявить protected методы, но если `final class`, то хоть protected хоть private, не имеет значения.


          1. MyraJKee
            05.11.2023 00:37

            Это какая-то маленькая программа, в которой не очень много бизнес-логики.

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


            1. AlexeyP0708 Автор
              05.11.2023 00:37

              Это какая-то маленькая программа, в которой не очень много бизнес-логики.

              А это уже принцип разделения Интерфейса в SOLID
              https://ru.wikipedia.org/wiki/Принцип_разделения_интерфейса


              1. MyraJKee
                05.11.2023 00:37

                Обычно все равно выходит больше 1 публичного метода. Взять например какую-нибудь корзину интернет магазина. Уже как минимум 2-3 публичных метода. А если добавить сервисы промокодов/баллов, сервис доставки и т.д. в итоге на практике получаются десятки публичных методов и несколько сервисов.


      1. tommyangelo27
        05.11.2023 00:37
        +1

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

        В общем случае, эта логика просто выносится в некий сервис и вы тестируете уже его. А класс с методом run() тестируете на запуск этого сервиса с нужными параметрами.


        1. MyraJKee
          05.11.2023 00:37

          Да, согласен. Обычно так и делаем. Но как правило если это не сферический конь в вакууме, то таких сервисов в программе десятки и даже в пределах одного домена может быть несколько сервисов одного уровня


        1. AlexeyP0708 Автор
          05.11.2023 00:37

          если эта логика не удовлетворяет правилу единственной ответственности, то так и делается. А если вы непосредственную связную логику выносите в отдельный сервис, то вы создаете излишние связи и плодите зависимости которые нужно поддерживать.

          Сейчас просто плодятся не нужные споры - а нужно ли тестировать приватные методы?

          А придет к вам клиент с какой нибудь госкорпорации и скажет - хочу 100% покрытие кода, плачу хорошие деньги. И вопрос само собой отпадет.

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