статические вызовы Как создать свой сайт > Вебмастеру > Создание своего сайта > Статические вызовы и тестирование

Статические вызовы и тестирование

Всякий прецедент опасен именно тем,
что он — прецедент, то есть реальность.
«Экспансия 1», Юлиан Семёнов.



Основная идея

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

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

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

    Исключение составляют статические методы, в которых все делегируемые классы и данные передаются в качестве параметров метода, или же достаются из какого-то источника, поведение которого можно легко контролировать в тесте.

Нарушение принципа инверсии зависимостей при использовании статических методов

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

    Такая зависимость нашего кода от класса со статическим методом является нарушением принципа инверсии зависимостей. В книге Роберта Мартина «Быстрая разработка программ» данный принцип раскрыт следующим образом: рекомендуется не использовать зависимость от статичного класса — все взаимоотношения в программе поддерживаются с помощью абстрактного класса или интерфейса. Тестирование позволяет находить такие места, где данное нарушение особо опасно для дизайна системы.

    Рассмотрим небольшой абстрактный пример. У нас есть класс A, который состоит из одного статичного метода, совершающего некоторые действия с базой данных. Также есть класс B, который зависит от результатов, полученных от класса A:

<?php
class A
{
  static public function doSomethingComplex() {
    // Implements some complex logic
  }
}
 
class B
{
  public function doSomethingUsefull() {
    $result = A :: doSomethingComplex();
 
    if ($result)
      return $this->_method1();
    else
      return $this->_method2();
  }
 
  protected function _method1(){...}
  protected function _method2(){...}
}
?>

    Тест на класс B схематично будет выглядеть таким образом:

<?php
class ClassBTest extends UnitTestCase
{
  function setUp(){
    $this->_fixtureForClassA();
    $this->_fixtureForClassB();
  }
 
  function testMethod1(){
    $this->_provideAResult1();
 
    $b = new B();
    $this->assert($b->doSomethingUsefull(), $as_method1);
  }
 
  function testMethod2(){
    $this->_provideAResult2();
 
    $b = new B();
    $this->assert($b->doSomethingUsefull(), $as_method2);
  }
}
?>

    Для тестирования методов B :: _method1() и B :: _method2() мы должны готовить такую фикстуру, которая бы обеспечивала определённый результат статического метода класса А. Не исключено, что для такого контроля размер фикстур будет значительным. То есть простую изоляцию тестируемого класса (класса B) провести не удаётся. В итоге тест будет бОльшим по размеру, чем он мог бы быть.

    Но тест на класс А будет выглядеть очень похоже:

<?php
class ClassATest extends UnitTestCase
{
  function setUp() {
    $this->_fixtureForClassA();
  }
 
  function testResult1() {
    $this->_provideAResult1();
 
    $this->assert(A :: doSomethingComplex(), $as_result1);
  }
 
  function testResult1() {
    $this->_provideAResult2();
 
    $this->assert(A :: doSomethingComplex(), $as_result2);
  }
}
?>

    Возникает дублирование фикстур. Тест на класс B знает слишком много и имеет зависимости от деталей класса А, поэтому при изменении класса А тест на класс B нужно будет также изменять. В итоге разработчики обычно решают не тестировать класс B, иначе поддержка теста на класс B может быть слишком дорогой (трудоёмкой). Если же разработчики всё же решают оставит оба теста, то при изменении класса А им приходится править тестовый код сразу в двух местах. Отсюда появляются отговорки, что тесты увеличивают стоимость поддержки системы в разы, теряется мобильность и т. д. Почему-то в качестве причины указываются именно тесты, а не недостатки архитектуры.

    Итак, необходимость протестировать этот код легко выявляет нарушение принципа инверсии зависимостей: класс B принадлежит к более высшему уровню, однако в данном случае он зависит от класса более низкого уровня A. Этого допускать нельзя.

Решение проблемы

    Для того, чтобы приведённый пример следовал принципу инверсии зависимостей, необходимо:

  • Ввести интерфейс, например, ComplexDoer.
  • Сделать так, чтобы класс A реализовывал этот интерфейс.
  • Создать в классе B метод setComplexDoer($doer) и передавать туда объект класса, поддерживающего интерфейс ComplexDoer. В нашем случае это может быть класс A, а в тесте мок на интефейс ComplexDoer. Также объект с интерфейсом ComplexDoer можно передавать в конструкторе класса B или же непосредственно в метод doSomethingUsefull() — всё зависит от вашего контекста.
  • Изменить поведение метода doSomethingUsefull() класса B так, чтобы в нём использовался объект, реализующий интерфейс ComplexDoer вместо вызова статичного метода класса A.

    Таким образом класс B больше не будет зависеть от класса A. Вместо этого класс A будет зависеть от интерфейса ComplexDoer. Единственная проблема здесь — PHP 4 не поддерживает интерфейсы и множественное наследование, поэтому нет никакой возможности на уровне парсера контролировать то, что класс A зависит от интефейса ComplexDoer. Такая возможность есть только при использовании PHP 5. Вот как должен измениться код:

<?php
interface ComplexDoer()
{
  function doSomethingComplex();
}
 
class A implements ComplexDoer
{
  function doSomethingComplex() {
    // Implements some complex logic
  }
}
 
class B
{
  private a;
 
  function __construct($a)
  {
    $this->a = $a;
  }
 
  public function doSomethingUsefull()
  {
    $result = $this->a->doSomethingComplex();
 
    if ($result)
      return $this->_method1();
    else
      return $this->_method2();
  }
 
  protected function _method1(){...}
  protected function _method2(){...}
}
?>

    Теперь тесты будут выглядеть совершенно по-иному:

<?php
Mock :: generate('ComplexDoer');
 
class ClassBTest extends UnitTestCase
{
  private $a;
 
  function setUp(){
    $this->a = new MockComplexDoer($this);
    $this->_fixtureForClassB();
  }
 
  function tearDown(){
    $this->a->tally();
  }
 
  function testMethod1(){
    $this->a->expectOnce('doSomethingComplex');
    $this->a->setReturnValue('doSomethingComplex', $result1);
 
    $b = new B($this->a);
    $this->assert($b->doSomethingUsefull(), $as_method1);
  }
 
  function testMethod2() {
    $this->a->expectOnce('doSomethingComplex');
    $this->a->setReturnValue('doSomethingComplex', $result2);
 
    $b = new B($this->a);
    $this->assert($b->doSomethingUsefull(), $as_method2);
  }
}
?>

    В тоже время тест на класс А будет выглядеть по-старому:

<?php
class ClassATest extends TestCase
{
  function setUp(){
    $this->_fixtureForClassA();
  }
 
  function testResult1() {
    $this->_provideAResult1();
 
    $a = new A();    
    $this->assert($a->doSomethingComplex(), $as_result1);
  }
 
  function testResult1() {
    $this->_provideAResult2();
 
    $a = new A();    
    $this->assert($a->doSomethingComplex(), $as_result2);
  }
}
?>

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

Автор: Вики.Агилдев.Ру.

Комментарии:


⇓ 

Поделись ссылкой на Seoded.ru с друзьями, знакомыми и собеседниками в соцсетях и на форумах! А сам сайт добавь в закладки! Так победим.

Поделиться ссылкой на эту страницу в:

Полезные ссылки:

Потестируй заработк на путешествиях Тестируем заработок для мамы в декрете

Ещё материалы по этой теме:

Прощай Smarty Графическое меню на CSS Позиционирование в CSS Наследование классов в Internet Explorer Вертикальное выравнивание текста в CSS
основан в 2008 г. © Все права на материалы сайта Seoded.ru принадлежат Алексею Вострову.
Копирование (полное или частичное) любых материалов сайта возможно только с разрешения автора и при указании ссылки на источник.
Ослушавшихся находит и забирает Бабайка!