[TDD] Как покрывать тестами private методы в классе?

confguru

ExAdmin
Команда форума
[TDD] Как покрывать тестами private методы в классе?

[TDD] Как покрывать тестами private методы в классе?
 

Wicked

Новичок
Имхо тестировать надо не классы, а интерфейсы. Поэтому ты не должен этого хотеть.
 

whirlwind

TDD infected, paranoid
Кстати, меня тоже интересует аналогичный вопрос. Допустим не приватные, а защищенные. В некоторых случаях определенные участки кода выносятся в отдельный метод.

Например doRequest используется несколькими методами. Мы вынесли doRequest в отдельный метод, что бы заткнуть его моком и проэмулировать ответ, когда тестируем методы использующие doRequest. Его было бы неплохъо сделать закрытым. Но тогда как его тестировать?

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

У кого какие идеи по этому поводу?
 

confguru

ExAdmin
Команда форума
Есть к примеру класс который потом вызывается
как $clas->run();
Внутри него есть приватные методы управляющие данными
 

Popoff

popoff.donetsk.ua
написать скрипт, который:
1. скопирует файл в безопасное место
2. сделает этот метод паблик
3. протестирует его так, как если бы это был паблик метод
 

kirill538

Новичок
А в тесткейсе наследовать тестируемый класс и сделать protected методы public по каким причинам 'попахивает' ?
 

Alexandre

PHPПенсионер
А в тесткейсе наследовать тестируемый класс и сделать protected методы public по каким причинам 'попахивает' ?
а так можно? protected, на то он и protected, чтоб был доступен только потомкам.

-~{}~ 22.02.08 15:55:

как выход вижу - временно расширить интерфейс класса:
PHP:
class Myclass {
...
public run(){
  $this->_run1();
  $this->_run2();
}
private _run1(){}
private _run2(){}

/* вводим временные методы */
public tmpRun1{
  return $this->_run1();
}

public tmpRun2{
  return $this->_run2();
}
}
Далее можно тестировать методы tmpRun1 & tmpRun2
по окончания теста, все что ниже коммента /* вводим временные методы */,
враппер на наши приватные методы удаляем.

это на много проще, чем
написать скрипт, который:
1. скопирует файл в безопасное место
2. сделает этот метод паблик
3. протестирует его так, как если бы это был паблик метод
Для протектед методов еще проще:
делаем временный класс TmpMyclass, наследник от Myclass ...
пишем паблик враппера на каждый протектед метод, далее осуществляем тестирование.
TmpMyclass - остается в test-case
не нужно все эти удаления.

Ну и остается как временное решение - временно их все переименовать в public.
 

itprog

Cruftsman
а зачем тестировать внутренности классов?
Тест пишется перед написанием класса, соответственно как он устроен внутри не должно быть известно.
Имхо тестировать надо не классы, а интерфейсы. Поэтому ты не должен этого хотеть.
+1
 

kirill538

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

atv

Новичок
Можно поподробнее, чем вызвана необходимость тестировать защищённый метод?
 

AmdY

Пью пиво
Команда форума
можно делать класс, который наследует тестируемый и позволяет работать с приватными данными через
__set, __get - они могут быть заняты :(
setProtected, getProtected - которые могут брать любые данные
setData и getData - которые могут брать только определённые данные.
class foo {
protected $_data;
}
class test_foo extends foo {
public function __set($data, $value) {}
public function __get($data) {}

public function setProtected($name, $data) {$this->{$name} = $value;}
public function getProtected($name) {return $this->{$name}}

public setData($value) {$this->_data = $value}
public getData {return $this->_data}
}
а, с приватными это не прокатит, нужно делать другой костыль
можно сделать
class foo {
protected $_data;
public function setProtected($name, $data) {if (__CLASS__ == 'test_foo') $this->{$name} = $value;}
public function getProtected($name) {if (__CLASS__ == 'test_foo') return $this->{$name}}
}
 

whirlwind

TDD infected, paranoid
Объясняю на пальцах почему нужно тестировать закрытые методы и почему эта ситуация частая

Вот 3 теста

PHP:
    public function testPayout(){
        ...
        $o = $this->getMock('PSO_LibertyReserve_Operator',
            Array('createPayoutRequest','doRequest',
                'analyseLrResponse','parseOperations'));
        ...
        $o->expects($this->once())
            ->method('doRequest')
            ->will($this->returnValue('<bar></bar>'));
        ...

        $this->assertTrue($o->payout($p,$acc));
    }

    public function testGetBalance(){
        ...
        
        $o = $this->getMock('PSO_LibertyReserve_Operator',
            Array('createBalanceRequest','doRequest',
                'analyseLrResponse','parseBalance'));
        ...
        $o->expects($this->once())
            ->method('doRequest')
            ->will($this->returnValue('<bar></bar>'));
        ...
        $amount = $o->getBalance($acc);
        $this->assertEquals(100500,$amount);
    }

    public function testSearchSubrequest(){
        ...
        $o->expects($this->once())
            ->method('doRequest')
            ->will($this->returnValue('<foo></foo>'));
        ...
    }
Метод doRequest должен быть защищенным, потому что это внутренности. Интерфес имплементируется следующий

PHP:
interface PSO_IOperator {
    
    public function payout(PSO_IPayment $payment,$account);
    public function getBalance($account,$force=false);
    public function search(PSO_ISearchCriteria $criteria,$account);
    
}
То есть, остальные методы лучше всего скрыть, т.к. они загрязняют интерфейс. Если вы способны реализовать этот интерфейс через TDD без защищенных методов и без дублирования кода, тогда остается только снять шляпу и пасть ниц. У меня в классе получилось 26 методов всего, из которых только 3 должны быть паблик.
 

atv

Новичок
Непонятно что за параметры передаются в метод getMock(), нужно было бы и его код привести.

Пока что проблема не понятна. Есть три паблик метода, для них есть тесты. В чём соль?

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

Fred

Новичок
Автор оригинала: whirlwind
Объясняю на пальцах почему нужно тестировать закрытые методы и почему эта ситуация частая

Вот 3 теста

PHP:
   ...

    public function testGetBalance(){
        ...
        
        $o = $this->getMock('PSO_LibertyReserve_Operator',
            Array('createBalanceRequest','doRequest',
                'analyseLrResponse','parseBalance'));
        ...
        $o->expects($this->once())
            ->method('doRequest')
            ->will($this->returnValue('<bar></bar>'));
        ...
        $amount = $o->getBalance($acc);
        $this->assertEquals(100500,$amount);
    }

   ...
Метод doRequest должен быть защищенным, потому что это внутренности. Интерфес имплементируется следующий

PHP:
interface PSO_IOperator {
    
    public function payout(PSO_IPayment $payment,$account);
    public function getBalance($account,$force=false);
    public function search(PSO_ISearchCriteria $criteria,$account);
    
}
То есть, остальные методы лучше всего скрыть, т.к. они загрязняют интерфейс. Если вы способны реализовать этот интерфейс через TDD без защищенных методов и без дублирования кода, тогда остается только снять шляпу и пасть ниц. У меня в классе получилось 26 методов всего, из которых только 3 должны быть паблик.
Из приведенного кода не очевидно, что закрытые методы тестировать надо. Смысл модульного тестирования - проверить, что класс правильно реагируют на внешнее воздействие.
Судя по имени функции, которую ты подменяешь (doRequest), она получает результат откуда-то извне (запрос к БД, curl, ....). В этом случае лучше лучше сделать инверсию зависимостей, вынести функционал по обращению к внешнему ресурсу в отдельный класс, а уже при тестировании этот класс подменять.

PHP:
interface PSO_IOperator {
    
    public function payout(PSO_IPayment $payment,$account);
    public function getBalance($account,$force=false);
    public function search(PSO_ISearchCriteria $criteria,$account);
 
    public function setRequestHandler(RequestHandlerInterface $handler) {
}
а затем в своих публичных методах

PHP:
class PSO_LibertyReserve_Operator implements PSO_IOperator {
    /**
     * @var RequestHandlerInterface
     */
    private $requestHandler;

    public function getBalance($account,$force=false) {
        ...
        $requestResult = $this->requestHandler->doRequest();
        ...
    }
}
Ну и тестировать это все проще и значительно нагляднее, что повышает ценность теста, как документации

PHP:
public function testGetBalance() {
    //fixture
    $requestMock = $this->getMock('RequestHandlerInterface');
    $requestMock->expects($this->once())
            ->method('doRequest')
            ->will($this->returnValue('<bar></bar>')); 
   
    $operator = new PSO_LibertyReserve_Operator();
    $operator->setRequestHandler($requestMock);

    //exercise
    $operator->getBalance($acc);
}
Ещё одна большая проблема при тестировании закрытых методов - они лишают нас возможности проводить полноценный рефакторинг. Если мы меняем внутреннее содержимое функции, то наши тесты ломаются, хотя внешнее поведение класса не изменилось. Т.е. одна из основных целей тестов - безболезненно проводить рефакторинг - не выполняется.
 

whirlwind

TDD infected, paranoid
atv getMock это метод PHPUnit::Framework::TestCase. За более подробной инфой http://phpunit.de

Смысл модульного тестирования - проверить, что класс правильно реагируют на внешнее воздействие.
Я не знаю, что такое модульное тестирование в твоем понимании. В теме стоит TDD - Test-Driven Development. Разработка на основе тестов, а не тестирование уже написанного. Разницу чуешь?

Судя по имени функции, которую ты подменяешь (doRequest), она получает результат откуда-то извне (запрос к БД, curl, ....). В этом случае лучше лучше сделать инверсию зависимостей, вынести функционал по обращению к внешнему ресурсу в отдельный класс, а уже при тестировании этот класс подменять.
С чего ты взял что лучше? Этот метод был введен намеренно, для того что бы избавиться от зависимости транспортного уровня в тестах, которые никакого отношения к этому самому транспорту не имеют. Где в коде видно, что в работе не используется впрыск зависимого объекта? Стабы для того и придумали, что бы не заморачиваться эмулированием всей необходимой фикстуры а просто настроить 1 метод.

Из приведенного кода не очевидно, что закрытые методы тестировать надо
То есть для тебя не очевидно, что метод, который в самом элементарном примере вызывается 3 раза должен быть протестирован? Это не говоря о том, что по TDD сначала появляется тест, а уже потом метод.
 

atv

Новичок
Разницу чуешь?
Это не разговор. Нет не чую, объясни.

Это не говоря о том, что по TDD сначала появляется тест, а уже потом метод.
Вот именно. Так как у тебя мог появиться метод doRequest(), до того как появились методы payout(), getBalance() и search()? Ведь реализация класса начинается именно с этих методов. Похоже, это ты занимаешься "тестированием уже написанного" кода.

То есть для тебя не очевидно, что метод, который в самом элементарном примере вызывается 3 раза должен быть протестирован?
А при чём здесь количество вызовов методов. Это у тебя такой подход к тестированию? Должен тебя огорчить, ты не понял смысла TDD, так как, скорее всего, не писал тесты до написания кода.
 

whirlwind

TDD infected, paranoid
atv не хотелось бы переходить на личности, но сдается мне человек, который не знает аргументов getMock похоже вообще тестов не писал ни до, ни после написания кода.

PS. Вообще тебе в моем посте адресована только первая строчка. Остальное относится к Fred
 
Сверху