Как использовать PHPUnit с CodeIgniter? - PullRequest
57 голосов
/ 06 декабря 2011

Я читал и читал статьи о PHPUnit, SimpleTest и других платформах модульного тестирования. Все они звучат так здорово! Я наконец получил PHPUnit, работающий с Codeigniter благодаря https://bitbucket.org/kenjis/my-ciunit/overview

Теперь мой вопрос: как мне его использовать?

Каждый учебник, который я вижу, имеет абстрактное использование, например assertEquals(2, 1+1) или:

public function testSpeakWithParams()
{
    $hello = new SayHello('Marco');
    $this->assertEquals("Hello Marco!", $hello->speak());
}

Хорошо, если бы у меня была функция, которая выводила бы такую ​​предсказуемую строку. Обычно мои приложения собирают данные из базы данных, а затем отображают их в виде таблицы. Так как же проверить контроллеры Codeigniter?

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

Есть ли книга или отличный учебник с практическим применением и примерами тестирования PHPUnit?

Ответы [ 2 ]

95 голосов
/ 06 декабря 2011

Кажется, вы понимаете базовую структуру / синтаксис написания тестов и модульного тестирования. Код CodeIgniter не должен ничем не отличаться от тестирования кода без CI, поэтому я хочу сосредоточиться на ваших основных проблемах / проблемах ...

У меня были подобные вопросы не так давно с PHPUnit. Как человек без формального обучения, я обнаружил, что мышление в модульном тестировании поначалу казалось абстрактным и неестественным. Я думаю, что основная причина этого - в моем случае, и, вероятно, ваша тоже из вопроса) - то, что вы до сих пор не сосредоточились на ДЕЙСТВИТЕЛЬНО работе над разделением проблем в вашем коде.

Утверждения тестирования кажутся абстрактными, потому что большинство ваших методов / функций, вероятно, выполняют несколько различных дискретных задач. Успешный менталитет тестирования требует изменения в том, как вы думаете о своем коде. Вы должны перестать определять успех с точки зрения "это работает?" Вместо этого вы должны спросить: «Работает ли он, будет ли он хорошо играть с другим кодом, разработан ли он таким образом, чтобы сделать его полезным для других приложений, и могу ли я проверить, работает ли он?»

Например, ниже приведен упрощенный пример того, как вы, вероятно, написали код до этого момента:

function parse_remote_page_txt($type = 'index')
{
  $remote_file = ConfigSingleton::$config_remote_site . "$type.php";
  $local_file  = ConfigSingleton::$config_save_path;

  if ($txt = file_get_contents($remote_file)) {
    if ($values_i_want_to_save = preg_match('//', $text)) {
      if (file_exists($local_file)) {
        $fh = fopen($local_file, 'w+');
        fwrite($fh, $values_i_want_to_save);
        fclose($fh);
        return TRUE;
      } else {
        return FALSE;
      }
  } else {
    return FALSE;
  }  
}

Точно, что здесь происходит, не важно. Я пытаюсь проиллюстрировать, почему этот код сложно протестировать:

  • Он использует класс конфигурации singleton для генерации значений. Успех вашей функции зависит от значений из синглтона, и как вы можете проверить, что эта функция работает правильно в полной изоляции, когда вы не можете создать новые объекты конфигурации с другими значениями? Лучшим вариантом может быть передача вашей функции аргумента $config, который состоит из объекта конфигурации или массива, значениями которого вы можете управлять. Это широко называется « Внедрение зависимостей », и обсуждаются эти методы во всех сетях.

  • Обратите внимание на вложенные IF операторы. Тестирование означает, что вы закрываете каждую исполняемую строку каким-то тестом. Когда вы вкладываете операторы IF, вы создаете новые ветви кода, которые требуют нового пути тестирования.

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


class RemoteParser() {
  protected $local_path;
  protected $remote_path;
  protected $config;

  /**
   * Class constructor -- forces injection of $config object
   * @param ConfigObj $config
   */
  public function __construct(ConfigObj $config) {
    $this->config = $config;
  }

  /**
   * Setter for local_path property
   * @param string $filename
   */
  public function set_local_path($filename) {
    $file = filter_var($filename);
    $this->local_path = $this->config->local_path . "/$file.html";
  }

  /**
   * Setter for remote_path property
   * @param string $filename
   */
  public function set_remote_path($filename) {
    $file = filter_var($filename);
    $this->remote_path = $this->config->remote_site . "/$file.html";
  }

  /**
   * Retrieve the remote source
   * @return string Remote source text
   */
  public function get_remote_path_src() {
    if ( ! $this->remote_path) {
      throw new Exception("you didn't set the remote file yet!");
    }
    if ( ! $this->local_path) {
      throw new Exception("you didn't set the local file yet!");
    }
    if ( ! $remote_src = file_get_contents($this->remote_path)) {
      throw new Exception("we had a problem getting the remote file!");
    }

    return $remote_src;
  }

  /**
   * Parse a source string for the values we want
   * @param string $src
   * @return mixed Values array on success or bool(FALSE) on failure
   */
  public function parse_remote_src($src='') {
    $src = filter_validate($src);
    if (stristr($src, 'value_we_want_to_find')) {
      return array('val1', 'val2');
    } else {
      return FALSE;
    }
  }

  /**
   * Getter for remote file path property
   * @return string Remote path
   */
  public function get_remote_path() {
    return $this->remote_path;
  }

  /**
   * Getter for local file path property
   * @return string Local path
   */
  public function get_local_path() {
    return $this->local_path;
  }
}

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

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

Чем больше вы будете тестировать, тем больше вы будете писать код и спрашивать себя: «Смогу ли я это проверить?» А если нет, то вы, вероятно, измените структуру тогда и там.

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

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

Наконец, вот пара ссылок, которые действительно помогли мне начать думать в удобной для тестирования форме. Первый - это ненормативный список того, что НЕ нужно делать, если вы хотите написать тестируемый код . На самом деле, если вы просматриваете весь этот сайт, вы найдете много полезных вещей, которые помогут вам перейти на 100% покрытие кода. Другая полезная статья - это обсуждение внедрения зависимостей .

Удачи!

2 голосов
/ 06 декабря 2011

Я безуспешно пытался использовать PHPUnit с Codeigniter. Например, если я хотел протестировать свои модели CI, я столкнулся с проблемой того, как я получу экземпляр этой модели, так как для ее загрузки каким-то образом требуется вся структура CI. Рассмотрим, как вы загружаете модель, например:

$this->load->model("domain_model");

Проблема в том, что если вы посмотрите на суперкласс для метода загрузки, вы не найдете его. Это не так просто, если вы тестируете Plain Old PHP Objects, где вы можете легко смоделировать свои зависимости и протестировать функциональность.

Следовательно, я согласился на Класс юнит тестирования CI .

my apps grab a bunch of data from the database then display it in some sort of table.

Если вы тестируете свои контроллеры, вы, по сути, тестируете там бизнес-логику (если она есть), а также SQL-запрос, «собирающий кучу данных» из базы данных. Это уже интеграционное тестирование.

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

Я предполагаю, что вы сначала хотите проверить получение данных, общие шаги:

  1. Получить тестовые данные и настроить базу данных, таблицы и т. Д.

  2. Иметь некоторый механизм для заполнения базы данных тестовыми данными, а также удалять их после теста. Расширение базы данных PHPUnit имеет способ сделать это, хотя я не знаю, поддерживается ли это фреймворком, который вы опубликовали. Дайте нам знать.

  3. Напишите свой тест, пройдите его.

Ваш метод тестирования может выглядеть следующим образом:

// At this point database has already been populated
public function testGetSomethingFromDB() {
    $something_model = $this->load->model("domain_model");
    $results = $something_model->getSomethings();
    $this->assertEquals(array(
       "item1","item2"), $results);

}
// After test is run database is truncated. 

На всякий случай, если вы хотите использовать класс модульного тестирования CI, вот модифицированный фрагмент кода одного теста, который я написал с его помощью:

class User extends CI_Controller {
    function __construct() {
        parent::__construct(false);
        $this->load->model("user_model");
        $this->load->library("unit_test");
    }

public function testGetZone() {
            // POPULATE DATA FIRST
    $user1 = array(
        'user_no' => 11,
        'first_name' => 'First',
        'last_name' => 'User'
    );

    $this->db->insert('user',$user1);

            // run method
    $all = $this->user_model->get_all_users();
            // and test
    echo $this->unit->run(count($all),1);

            // DELETE
    $this->db->delete('user',array('user_no' => 11));

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...