kix

Ссылки

PHP

Ruby/Rails

Веб

  • Parker — инструмент для статического анализа CSS, можно использовать для регрессного тестирования и подобных вещей.

Прочее

Ссылки

  • Наконец-то запилили плагин для PHPStorm, который вскрывает симфонийский контейнер и рисует подсказки для объектов, возвращающихся из $container->get('')
  • Волшебный бандлоподключатель вот еще сделали — толку, правда, немного, все равно конфиги-то править надо, плюс еще головняки с приоритетами рутов. Но штука давно напрашивалась.
  • Прицепили Bower через бандл, тонкостей не смотрел, но — радует. И мне как-то пофиг, я скрипт на Grunt запилил для сборки статики, и верстка не сильно заботит.
  • Я конечно слоупок, но доктриновские расширения тоже давно хотели быть завернутыми в примеси. Вот, сделали наконец.
  • Скромная полезняшка — HTTP status codes, обернутые в класс с константами.
  • Вебсокеты для PHP. Да, знаю, что боянище.
  • Наконец-то, человеческий легковесный дата-маппер, пусть и немного страшненький. Для некрупных проектов, где лезть через DBAL стыдно, а Доктрину цеплять лень — самое то.
  • Детектор регрессии для PHP со статистикой и поэтессами. Чтобы джуниоров по рукам бить!

Mockery: вкуснота

Поскольку я тут продолжаю писать про тестирование, не могу не упомянуть о прекрасной библиотеке Mockery. Это отличная реализация мок-объектов, которой в разы удобнее пользоваться, в сравнении со средствами PHPUnit, по крайней мере.

Чтобы получить Mockery в проект, надо в блок require в composer.json вписать вот такую вот строчку:

1
2
3
    "require": {
        "mockery/mockery": "dev-master"
    }

Сами авторы пока что советуют не привязываться к конкретной версии, хотя теги в репозитории есть (на момент написания заметки самая свежая версия — 0.7.2). Очевидно, проект еще не очень стабилен, но в любом случае очень удобен. Используется он например вот так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
use \Mockery as m;

$geocoded = new \Geocoder\Result\Geocoded();
$geocoded->fromArray(array(
    'latitude'  => 12,
    'longitude' => 34,
    'city'      => 'Test city',
));

$geocoderInterfaceMock = m::mock('\Geocoder\GeocoderInterface')
                          ->shouldReceive('geocode')
                          ->with('46.48.48.87')
                          ->andReturn($geocoded)
                          ->getMock();

$geocoderMock = m::mock('\Geocoder\GeocoderInterface')
                 ->shouldReceive('using')->once()->with(m::type('string'))
                 ->andReturn($geocoderInterfaceMock)->getMock();

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

Обратите внимание, какой симпатичный (и человекопонятный!) у Mockery API: все методы называются так, как они и должны называться, все нужное выведено в статические функции у легкодоступного класса, словом — сказка, а не моки.

Собственно, что к чему в коде.

Метод mock('\Имя\Класса\Как\Строка') дает нам мок-объект, над которым мы впоследствии можем всячески издеваться. shouldReceive('имяМетода') добавляет в мок ожидание и возвращает его в стиле fluent interface. Этот самый fluent interface позволяет настроить ожидание.

Метод ожидания with() принимает как точные значения параметров, так и Matcher. Matcher по сути является вот таким куском кода:

Mockery\Matcher\Type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

namespace Mockery\Matcher;

class Type extends MatcherAbstract
{
    /**
     * Check if the actual value matches the expected.
     *
     * @param mixed $actual
     * @return bool
     */
    public function match(&$actual)
    {
        $function = 'is_' . strtolower($this->_expected);
        if (function_exists($function)) {
            return $function($actual);
        } elseif (is_string($this->_expected)
        && (class_exists($this->_expected) || interface_exists($this->_expected))) {
            return $actual instanceof $this->_expected;
        }
        return false;
    }

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

Также в создании $geocoderMock я после вызова shouldReceive() указываю, что метод using в имитируемом классе должен быть вызван один раз. И делаю это простым вызовом \Mockery\Expectation::once(), что несомненно куда как приятнее, чем стиль PHPUnit. А вот кстати полный набор методов для указания количества вызовов.

С методом andReturn(), думаю, все понятно — он принимает очередь параметров. В моем случае очередь состоит всегда из одного параметра, потому что тестируется достаточно простой функционал, не требующий особых танцев с бубном.

Ну и небольшой нюанс в том, что после объявления ожиданий мока последним значением, вернувшимся в нашем fluent interface, является ожидание, а не сам мок, поэтому в конце вызова нужно вызвать еще и метод \Mockery\Expectation::getMock().

Конечно, это ваше дело, чем пользоваться — Mockery или средствами, встроенными в PHPUnit, но все же мне кажется, что эта библиотека более чем достойна упоминания.

Примеси в PHP 5.4

Сегодня задался вопросом о том, почему мой проект на Symfony 2 все еще целится в PHP 5.3 как основную версию. Ну и предположил, что, вероятно, dev-часть можно смело переводить на 5.4 и использовать в тестах все специфичные для новой версии плюшки.

Начал я с трейтов, и нашел для них пока что как минимум одно хорошее применение: тесты.

Поскольку приложение симфонийское, постоянно приходится при запуске тестов этот самый контейнер доставать из static::createClient. Этот момент я реализовал в виде вот такой примеси:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
/**
 * WebTestCase client-aware test case
 */
trait ClientAware
{
    /**
     * @var \Symfony\Bundle\FrameworkBundle\Client
     */
    private $client;

    /**
     * Sets up client
     */
    public function setUp()
    {
        $this->client = static::createClient();
    }

    /**
     * @return \Symfony\Component\DependencyInjection\ContainerInterface
     */
    public function getContainer()
    {
        return $this->client->getContainer();
    }
}

Как можно заметить, я не стал здесь отступать от использования метода setUp(), чтобы PHPUnit меня правильно понял, и мне не пришлось в каждом тесте городить свой setUp() с блекджеком и вызовом метода вроде createClient(). Ну и поскольку контейнер тоже приходится получать довольно часто, я вывел еще и шорткат-метод для этой самой цели.

А еще почти что во всех тестах, связанных с моделями, приходится очищать БД в методе setUp(). Однако это довольно-таки много кода, и остается либо вынести всю логику в класс-наследник WebTestCase, или копипастить код. Меня такое решение не устроило, и я решил, что тут не помешает трейт:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
use \Doctrine\Common\DataFixtures;

/**
 * Purges DB, needs container
 */
trait PurgeDb
{

    use ClientAware {
        setUp as clientAwareSetUp;
    }

    /**
     * Calls ClientAware trait method
     */
    public function setUp()
    {
        $this->clientAwareSetUp();

        $em = $this->getContainer()->get('doctrine.orm.entity_manager');
        $purger = new DataFixtures\Purger\ORMPurger($em);
        $purger->purge();
    }

}

Тут надо обратить внимание на строчки 9-11. В них конструкция, которая позволяет одной примеси включить в себя другую в случае, когда их имена методов совпадают. И поскольку в PurgeDb мне однозначно нужен контейнер, я вызываю метод ClientAware::setUp().

Дальше все достаточно очевидно: поскольку контейнер у меня уже есть, я могу смело достать из него Entity Manager и скормить его в Purger, который затем чистит мою БД.

А теперь, собственно, как все это использовать? Вот тест, которому перед запуском нужна пустая база:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
use \Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

use PurgeDb;

class FakerTest extends WebTestCase
{

    use PurgeDb;

    public function testPurgeAndPopulateEntity()
    {
        /** @var $em \Doctrine\ORM\EntityManager */
        $em = $this->getContainer()->get('doctrine.orm.entity_manager');
        $this->assertEquals(
            0,
            count($em->getRepository('WtProMainBundle:User')->findAll())
        );

        $faker = new \My\Faker($this->getContainer());

        $populator = $faker->getPopulator();
        $populator->addEntity('Entity\User', 3);
        $populator->execute(
            $this->getContainer()->get('doctrine.orm.entity_manager')
        );

        $this->assertEquals(
            3,
            count($em->getRepository('WtProMainBundle:User')->findAll())
        );
    }
}

Как можно заметить, метод setUp() в нем вообще отсутствует. А поскольку PurgeDb не только чистит БД, но и вызывает setUp() из ClientAware, в моем тесте я могу всегда получить контейнер легко и непринужденно.