Блог Мазепина Василия

Пишу о том, что кажется интересным

PHPUnit + Symfony3 + MongoDB

2016-06-21 23:55 | Комментарии

Доброго времени суток, коллеги и гости. Некоторое время назад решил я для своего проекта взять за правило написание тестов.
Хотелось поделиться с вами своим опытом подготовки проекта (Symfony 3 + MongoDB) для написания тестов.

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

Шаг первый. Предисловие

Использовать девелоперскую БД мне бы не хотелось, так как мало ли какие действия тесты будут делать с данными(добавит мусора, изменят нужное, и т.д.) Поэтому в Symfony-проекте пишем: cp app/config/parameters.yml app/config/parameters_test.yml И в этом файле мы укажем новые данные доступа к уже другой БД, которая будет использоваться сугубо для тестов И не забываем файл этот в .gitignore добавить, в репозитории он не нужен.

Шаг второй. Слияние

Теперь надо бы, чтобы этот файл благополучно читался в конфиге, допишем его чтение в app/config/config_test.yml (В стандартной поставке Symfony у меня этот файл уже был):

app/config/config_test.yml
1
2
3
4
imports:
    - { resource: config_dev.yml }
    - { resource: parameters_test.yml } # именно вот эту строчку добавить надо
...

Шаг третий. Кардинальненько

Теперь надо бы, чтобы вся эта конфигурация успешно считывалась для тестов. Для этого достаточно:
1. cp web/app_dev.php web/app_test.php
2. И потом:

web/app_test.php
1
2
3
4
5
6
7
8
9
// Здесь всякое разное
$loader = require __DIR__.'/../app/autoload.php';
Debug::enable();
// А ты, мой друг, обрати внимание на эту строку, тут грузится ядро с тестовой конфигурацией
$kernel = new AppKernel('test', true);
$kernel->loadClassCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
// и там еще что то


3. Дайте права на запись кеша для тестового окружения (если необходимо)
4. Дайте права на запись логов для тестового окружения (если необходимо)
Кажется, всё. НО НЕТ!
5. А ваш веб-сервер знает об новом FrontController'e ? Дам свой рабочий конфиг для связки php-fpm и nginx'a, просто добавьте эту часть в server

/etc/nginx/sites-enabled/файл_конфига
1
2
3
4
5
6
7
8
9
10
// Здесь всякое разное
$loader = require __DIR__.'/../app/autoload.php';
Debug::enable();
location ~ ^/(app_test)\.php(/|$) {
     fastcgi_pass unix:/run/php/php5.6-fpm.sock;
     fastcgi_split_path_info ^(.+\.php)(/.*)$;
     include fastcgi_params;
     fastcgi_param  SCRIPT_FILENAME  $realpath_root$fastcgi_script_name;
     fastcgi_param DOCUMENT_ROOT $realpath_root;
 }

Теперь, по идее, можно с браузера зайти на свой локальный адрес проекта http://example.dev/app_test.php/example
А теперь самое интересное - написание тестов.
Не буду останавливаться на стандартных, аля тестирование главной страницы. Пройдусь по работе с БД.
Так как я использую MongoDB, а не MySQL, то мне не пришлось возиться с миграциями, настраивать и синхронизировать схему данных и прочее, все тут оказалось гораздо проще. Теперь, когда мое тестовое окружение работает с абсолютно другой БД, я могу беспрепятственно делать необходимые мне записи в БД, не боясь поломать данные на дев-машине.
Приведу пример:

tests/YourBundle/Controller
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<?php

namespace Tests\SafePageBundle\Controller;

use SafePageBundle\Document\Entity\OtherEntity;
use SafePageBundle\Document\Entity\Entity;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class CheckControllerTest extends WebTestCase
{

    /**
     * Ожидаем 200 ответ и дополнительные данные в теле ответа
     */
    public function testUrlIsOtherEntity()
    {
        $client = self::createClient();
        $this->recordEntity('example.com/page');
        $client->request('get', $this->getRouter()->generate('api_check'), ['url' => 'http://example.com/page?page=1']);
        $this->assertEquals(200, $client->getResponse()->getStatusCode());
        $content = $client->getResponse()->getContent();
        $this->assertJson($content);
        $json = json_decode($content, true);
        $this->assertEquals(200, $json['status']);
        $this->assertEquals(200, $json['data']['code']);
    }

    /**
     * Ожидаем 200 ответ и дополнительные данные в теле ответа
     */
    public function testUrlIsEntity()
    {
        $client = self::createClient();
        $this->recordOtherEntity('example.com');
        $client->request('get', $this->getRouter()->generate('api_check'), ['url' => 'http://example.com/page?page=1']);
        $this->assertEquals(200, $client->getResponse()->getStatusCode());
        $content = $client->getResponse()->getContent();
        $this->assertJson($content);
        $json = json_decode($content, true);
        $this->assertEquals(200, $json['data']['code']);
    }

    /**
     * Очистка БД после каждого теста
     */
    public function tearDown()
    {
        $this->removeAllCollection(Entity::class);
        $this->removeAllCollection(OtherEntity::class);
    }

    /**
     * Получение DocumentManager'a
     * @return \Doctrine\Common\Persistence\ObjectManager|object
     */
    public function getDM()
    {
        $client = self::createClient();
        return $client->getContainer()->get('doctrine_mongodb')->getManager();
    }

    /**
     * Очищает всю коллекцию
     * @param string $collection Класс модели для очистки
     */
    public function removeAllCollection($collection)
    {
        $dm = $this->getDM();
        $entities = $dm->getRepository($collection)->findAll();
        foreach ($entities as $entity) {
            $dm->remove($entity);
        }
        $dm->flush();
    }

    /**
     * Получение Роутера
     * @return \Symfony\Bundle\FrameworkBundle\Routing\Router
     */
    public function getRouter()
    {
        $client = self::createClient();
        return $client->getContainer()->get('router');
    }

    /**
     * Получение параметров из конфигурации
     *
     * @param $parameter
     * @return string | integer | null
     */
    public function getParam($parameter)
    {
        $client = self::createClient();
        return $client->getContainer()->getParameter($parameter);
    }

    /**
     * Получение клиента, который может авторизоваться через Basic Authentication
     * @return \Symfony\Bundle\FrameworkBundle\Client
     */
    public function getAuthClient()
    {
        $client = static::createClient(array(), array(
            'PHP_AUTH_USER' => $this->getParam('admin_login'),
            'PHP_AUTH_PW'   => $this->getParam('admin_password'),
        ));
        return $client;
    }

    /** Запись такая же, как в обычных контроллерах */
    public function recordEntity($page, $level = '',  \DateTime $created = null)
    {
        $entity = new Entity();
        if (!$created || $created = new \DateTime()) {
            $entity->setCreatedAt($created);
        }
        $entity->setLevel($level);
        $entity->setPage($page);
        $dm = $this->getDM();
        $dm->persist($entity);
        $dm->flush();
    }

    public function recordOtherEntity($domain, $level = '',  \DateTime $created = null)
    {
        $otherEntity = new OtherEntity();
        if (!$created || $created = new \DateTime()) {
            $otherEntity->setCreatedAt($created);
        }
        $otherEntity->setLevel($level);
        $otherEntity->setDomain($domain);
        $dm = $this->getDM();
        $dm->persist($otherEntity);
        $dm->flush();
    }
}

Как можно заметить, у нашего $client есть доступ в контейнер сервисов, и мы можем через доктрину аналогично контроллерам писать в БД, которая отделена от девелоперской. Кроме этого, мы ее еще и затирать можем без боязни (если с умом подходить к вопросу). К слову сказать, тесты у меня запускаются просто: phpunit -c ./

Что в итоге?

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

Комментарии