Статьи

Автоматическое тестирование на TDD с PHP

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


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

В этом руководстве для примера кода используется PHP, однако методы применимы для любого языка, который предлагает утилиту CLI для модульного тестирования. Требуется Ruby, потому что мы будем использовать гем watchr. Итак, убедитесь, что у вас есть работающая установка Ruby и PHP с PHPUnit .

Затем убедитесь, что у вас установлен libnotify , если вы работаете в Linux; Пользователям Windows и Mac OSX требуется «Growl». Этот учебник применим непосредственно в Linux, но я буду предлагать альтернативные команды и настройки, где это возможно.

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

1
gem install watchr

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

Watchr gem — это исполняемая программа, написанная на Ruby, и она охватывает функции, обнаруженные в файловой системе операционной системы, чтобы обеспечить возможность отслеживания изменений, внесенных в конкретный файл или папку. Естественно, эти функции файловой системы различаются для каждой операционной системы и файловой системы.

watchr предоставляет единый интерфейс прикладного программирования (API) для всех операционных систем. В Linux он использует inotify , библиотеку событий файловой системы ядра; в других операционных системах используется соответствующая альтернатива. Если по какой-либо причине в операционной системе нет доступной службы событий, средство наблюдения периодически опрашивает просматриваемый файл или папку.

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


Наш проект довольно прост. Скопируйте простую структуру каталогов, показанную на следующем рисунке:

Новый проект PHP

В файле Nettuts.php добавьте следующий код:

1
2
3
4
5
6
7
<?php
 
class Nettuts {
 
}
 
?>

Затем добавьте следующий код в NettutsTest.php :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?php
 
require_once dirname(__FILE__) .
 
class NettutsTest extends PHPUnit_Framework_TestCase {
 
  protected $object;
 
  protected function setUp() {
    $this->object = new Nettuts;
  }
 
  protected function tearDown() {
 
  }
}
 
?>

На данный момент тестовый файл — это просто скелет, и, как вы можете видеть на рисунке выше, тесты проходят.


Теперь нам нужно создать файл Ruby в папке нашего проекта; давайте назовем это autotest_watchr.rb . Затем добавьте следующий код в файл:

1
2
3
watch(«Classes/(.*).php») do |match|
  run_test %{Tests/#{match[1]}Test.php}
end

Автоматизированные тесты независимы от IDE — большой плюс в моей книге.

Этот код использует метод watch для просмотра всех файлов .php в папке Classes нашего проекта. При изменении файла .php операционная система выдает событие, и наш метод watch будет запущен. Имя файла .php возвращается (за вычетом расширения) в позиции массива соответствия 1 . Как и с любым регулярным выражением, круглые скобки используются для указания переменной соответствия, и в этом коде мы используем их в условии соответствия для получения имени файла. Затем мы вызываем метод run_test с путем к имени run_test тестового файла.

Мы также должны посмотреть наши тестовые файлы; Итак, добавьте следующий код в файл Ruby:

1
2
3
watch(«Tests/.*Test.php») do |match|
  run_test match[0]
end

Обратите внимание, что массив match содержит полное имя файла в позиции 0 , и мы передаем его непосредственно методу run_test .


Сценарий Ruby настроен для просмотра наших файлов .php , и теперь нам нужно реализовать метод run_test . В нашем случае мы хотим запустить PHPUnit для конкретного файла.

01
02
03
04
05
06
07
08
09
10
def run_test(file)
  unless File.exist?(file)
    puts «#{file} does not exist»
    return
  end
 
  puts «Running #{file}»
  result = `phpunit #{file}`
  puts result
end

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

1
watchr ./autotest_watchr.rb

Пользователи Windows должны опустить «./» в приведенной выше команде.

Теперь измените один из файлов .php (просто добавьте пустую строку в конце файла), сохраните его и просмотрите вывод в консоли. Вы должны увидеть нечто похожее на то, что показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
Running Tests/NettutsTest.php
PHPUnit 3.6.0 by Sebastian Bergmann.
 
F
 
Time: 0 seconds, Memory: 3.75Mb
 
There was 1 failure:
 
1) Warning
No tests found in class «NettutsTest».
 
/usr/bin/phpunit:46
 
FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

Да, у нас еще нет теста для запуска; так что давайте введем фиктивный тест. Добавьте следующий код в тестовый файл PHP:

1
2
3
function testDummyPassingTest() {
  $this->assertTrue(true);
}

Запустите скрипт Ruby еще раз, и вы должны увидеть:

1
2
3
4
5
6
7
8
Running Tests/NettutsTest.php
PHPUnit 3.6.0 by Sebastian Bergmann.
 
.
 
Time: 0 seconds, Memory: 3.75Mb
 
OK (1 test, 1 assertion)

Давайте уведомим пользователя через механизм уведомления системы о результатах теста. Мы run_tests метод run_tests чтобы вызвать метод, называемый notify . Ниже приведен модифицированный run_tests :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
def run_tests(file)
  unless File.exist?(file)
    puts «#{file} does not exist»
    return
  end
 
  puts «Running #{file}»
  result = `phpunit #{file}`
  puts result
 
  if result.match(/OK/)
    notify «#{file}», «Tests Passed Successfuly», «success.png», 2000
  end
end

Имя файла изображения, success.png , указывает на изображение, которое вы хотите отобразить в области уведомлений. Это изображение не предоставляется в этом руководстве; так что вам нужно будет найти свой собственный. Теперь давайте напишем метод notify :

1
2
3
4
def notify title, msg, img, show_time
  images_dir=’~/.autotest/images’
  system «notify-send ‘#{title}’ ‘#{msg}’ -i #{images_dir}/#{img} -t #{show_time}»
end

Пользователи Mac OSX и Windows: замените команду notify-send соответствующей альтернативой Growl. Измените что-либо в своем тестовом файле или файле кода, чтобы тест все еще проходил. Сохраните измененный файл PHP и наблюдайте, как происходит волшебство. Ниже приведено изображение результата в моей системе:

Испытания пройдены успешно

Далее нам нужно ловить сбои. Следующий код добавляет пару строк в run_tests :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
def run_tests(file)
  unless File.exist?(file)
    puts «#{file} does not exist»
    return
  end
 
  puts «Running #{file}»
  result = `phpunit #{file}`
  puts result
 
  if result.match(/OK/)
    notify «#{file}», «Tests Passed Successfuly», «success.png», 2000
  elsif result.match(/FAILURES\!/)
    notify_failed file, result
  end
end

Кроме того, давайте добавим метод notify_failed в файл:

1
2
3
4
def notify_failed cmd, result
  failed_examples = result.scan(/failure:\n\n(.*)\n/)
  notify «#{cmd}», failed_examples[0], «failure.png», 6000
end

Измените любой из ваших файлов PHP, чтобы сделать тест неудачным; сохраните измененный файл. Соблюдайте уведомление. Он содержит имя первого провального теста. Это имя выбирается регулярным выражением в методе notify_failed , который анализирует выходные данные PHPUnit.

Тесты не пройдены

Добавьте следующий метод в ваш скрипт на Ruby и обязательно вызовите его в методе run_test . Код должен работать в Linux и Mac OSX, хотя вам может потребоваться провести некоторые исследования для Windows.

1
2
3
def clear_console
  puts «\e[H\e[2J» #clear console
end

Всякий раз, когда вы программируете с использованием TDD, любой инструмент, который помогает вам получить более быструю обратную связь, является ценным активом. Мои коллеги используют похожие скрипты с наблюдателем или альтернативами (некоторые написаны вокруг fs_event на MacOS). Излишне говорить, что мы сейчас избалованы и не можем представить, что будем разрабатывать что-либо без автоматического запуска тестов.

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