Статьи

Тестирование контроллеров Laravel

Тестирование контроллеров не самая простая вещь в мире. Что ж, позвольте мне перефразировать это: тестирование их — это несложно; По крайней мере, на первый взгляд сложно определить, что тестировать.

Должен ли контроллер проверять текст на странице? Должно ли это коснуться базы данных? Должно ли оно гарантировать наличие переменных в представлении? Если это ваша первая поездка на сене, эти вещи могут сбить с толку! Позвольте мне помочь.

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

Процесс тестирования контроллера можно разделить на три части.

  • Изолировать: макетировать все зависимости (возможно, исключая View ).
  • Вызов: вызвать нужный метод контроллера.
  • Убедитесь: выполните утверждения, убедившись, что этап был установлен правильно.

Лучший способ научиться этому — на примерах. Вот » привет мир » тестирования контроллера в Laravel.

01
02
03
04
05
06
07
08
09
10
11
12
<?php
 
# app/tests/controllers/PostsControllerTest.php
 
class PostsControllerTest extends TestCase {
 
  public function testIndex()
  {
      $this->client->request(‘GET’, ‘posts’);
  }
 
}

Laravel использует несколько компонентов Symfony для упрощения процесса тестирования маршрутов и представлений, включая HttpKernel, DomCrawler и BrowserKit. Вот почему крайне важно, что ваши тесты PHPUnit наследуются не от PHPUnit\_Framework\_TestCase , а от TestCase . Не волнуйтесь, Laravel по-прежнему расширяет первое, но помогает настроить приложение Laravel для тестирования, а также предоставляет различные вспомогательные методы подтверждения, которые вам рекомендуется использовать. Подробнее об этом в ближайшее время.

В приведенном выше фрагменте кода мы делаем GET запрос к /posts или localhost:8000/posts . Предполагая, что эта строка добавлена ​​в новую установку Laravel, Symfony выдаст NotFoundHttpException . Если вы работаете вместе, попробуйте запустить phpunit из командной строки.

1
2
3
$ phpunit
1) PostsControllerTest::testIndex
Symfony\Component\HttpKernel\Exception\NotFoundHttpException:

По- человечески это по сути означает: « Эй, я пытался назвать этот маршрут, но у тебя ничего не зарегистрировано, дурак! »

Как вы можете себе представить, этот тип запроса достаточно распространен настолько, что имеет смысл предоставить вспомогательный метод, такой как $this->call() . На самом деле, Laravel делает именно это! Это означает, что предыдущий пример может быть реорганизован следующим образом:

1
2
3
4
5
6
#app/tests/controllers/PostsControllerTest.php
 
public function testIndex()
{
    $this->call(‘GET’, ‘posts’);
}

Хотя мы будем придерживаться базовой функциональности в этой главе, в моих личных проектах я продвигаюсь дальше, допуская такие методы, как $this->get() , $this->post() и т. Д. Благодаря Перегрузка PHP, для этого требуется добавить только один метод, который вы можете добавить в app/tests/TestCase.php .

01
02
03
04
05
06
07
08
09
10
11
# app/tests/TestCase.php
 
public function __call($method, $args)
{
    if (in_array($method, [‘get’, ‘post’, ‘put’, ‘patch’, ‘delete’]))
    {
        return $this->call($method, $args[0]);
    }
 
    throw new BadMethodCallException;
}

Теперь вы можете написать $this->get('posts') и достичь того же результата, что и в предыдущих двух примерах. Однако, как отмечалось выше, для простоты давайте придерживаться базовой функциональности фреймворка.

Чтобы пройти тест, нам нужно только подготовить правильный маршрут.

1
2
3
4
5
6
7
8
<?php
 
# app/routes.php
 
Route::get(‘posts’, function()
{
    return ‘all posts’;
});

Запуск phpunit снова вернет нас к зеленому phpunit .


Тест, который вы будете неоднократно писать, гарантирует, что контроллер передает определенную переменную в представление. Например, index метод PostsController должен передать переменную $posts в соответствующее представление, верно? Таким образом, представление может фильтровать все сообщения и отображать их на странице. Это важный тест для написания!

Если это обычная задача, то, опять же, не имеет ли смысла для Laravel предоставить вспомогательное утверждение для выполнения этой самой задачи? Конечно, будет. И, конечно же, Laravel делает!

Illuminate\Foundation\Testing\TestCase включает в себя ряд методов, которые позволят значительно сократить объем кода, необходимого для выполнения основных утверждений. Этот список включает в себя:

  • assertViewHas
  • assertResponseOk
  • assertRedirectedTo
  • assertRedirectedToRoute
  • assertRedirectedToAction
  • assertSessionHas
  • assertSessionHasErrors

В следующих примерах вызывается GET /posts и проверяется, получает ли его представление переменную $posts .

1
2
3
4
5
6
7
8
# app/tests/controllers/PostsControllerTest.php
 
public function testIndex()
{
    $this->call(‘GET’, ‘posts’);
 
    $this->assertViewHas(‘posts’);
}

Совет: когда дело доходит до форматирования, я предпочитаю указывать разрыв строки между утверждением теста и кодом, который готовит этап.

assertViewHas — это просто кусочек сахара, который проверяет объект ответа — который возвращается из $this->call() — и проверяет, что данные, связанные с представлением, содержат переменную posts .

При проверке объекта ответа у вас есть два основных варианта.

  • $response->getOriginalContent() : получить исходное содержимое или возвращенное View . При желании вы можете получить доступ к original свойству напрямую, а не вызывать метод getOriginalContent .
  • $response->getContent() : получить обработанный вывод. Если экземпляр View возвращается из маршрута, то getContent() будет равен выводу HTML. Это может быть полезно для проверок DOM, таких как « представление должно содержать эту строку ».

Предположим, что маршрут posts состоит из:

1
2
3
4
5
6
7
8
<?php
 
# app/routes.php
 
Route::get(‘posts’, function()
{
    return View::make(‘posts.index’);
});

Если мы запустим phpunit , он выдаст полезное сообщение следующего шага :

1
2
1) PostsControllerTest::testIndex
Failed asserting that an array has the key ‘posts’.

Чтобы сделать его зеленым, мы просто выбираем посты и передаем их на просмотр.

1
2
3
4
5
6
7
8
# app/routes.php
 
Route::get(‘posts’, function()
{
    $posts = Post::all();
 
    return View::make(‘posts.index’, [‘posts’, $posts]);
});

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

1
2
3
4
5
6
7
8
# app/tests/controllers/PostsControllerTest.php
 
public function testIndex()
{
    $this->call(‘GET’, ‘posts’);
 
    $this->assertViewHas(‘posts’, ‘foo’);
}

С этим измененным кодом unles представление имеет переменную $posts , равную foo , тест не пройден. Однако в этой ситуации, скорее всего, мы не будем указывать значение, а вместо этого объявляем, что это значение является экземпляром класса Laravel Illuminate\Database\Eloquent\Collection . Как мы можем достичь этого? PHPUnit предоставляет полезное утверждение assertInstanceOf для удовлетворения этой самой потребности!

01
02
03
04
05
06
07
08
09
10
11
12
13
# app/tests/controllers/PostsControllerTest.php
 
public function testIndex()
{
    $response = $this->call(‘GET’, ‘posts’);
 
    $this->assertViewHas(‘posts’);
 
    // getData() returns all vars attached to the response.
    $posts = $response->original->getData()[‘posts’];
 
    $this->assertInstanceOf(‘Illuminate\Database\Eloquent\Collection’, $posts);
}

С этой модификацией мы объявили, что контроллер должен передать $posts — экземпляр Illuminate\Database\Eloquent\Collection — в представление. Отлично.


Пока что есть одна вопиющая проблема с нашими тестами. Ты поймал это?

Для каждого теста в базе данных выполняется SQL-запрос. Хотя это полезно для определенных видов тестирования (приемочное, интеграционное), для базового тестирования контроллера это только приведет к снижению производительности.

Я уже несколько раз пробурил это в вашем черепе. Мы не заинтересованы в тестировании способности Eloquent получать записи из базы данных. У него свои тесты. Тейлор знает, что это работает! Давайте не будем тратить время и вычислительные мощности на повторение тех же тестов.

Вместо этого лучше всего смоделировать базу данных и просто проверить, что соответствующие методы вызываются с правильными аргументами. Или, другими словами, мы хотим убедиться, что Post::all() никогда не запускается и не попадает в базу данных. Мы знаем, что это работает, поэтому не требует тестирования.

Этот раздел будет сильно зависеть от библиотеки Mockery. Пожалуйста, просмотрите эту главу из моей книги , если вы еще не знакомы с ней.

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

01
02
03
04
05
06
07
08
09
10
# app/routes.php
 
Route::get(‘posts’, function()
{
    // Ouch.
    $posts = Post::all();
 
    return View::make(‘posts.index’)
        ->with(‘posts’, $posts);
});

Именно поэтому считается плохой практикой вкладывать вызовы Eloquent в ваши контроллеры. Не путайте фасады Laravel, которые можно тестировать и которые можно поменять местами с Queue::shouldReceive() ), с вашими моделями Eloquent. Решение состоит в том, чтобы внедрить слой базы данных в контроллер через конструктор. Это требует некоторого рефакторинга.

Предупреждение. Хранение логики в обратных вызовах маршрута полезно для небольших проектов и API, но делает тестирование невероятно трудным. Для приложений любого значительного размера используйте контроллеры.

Давайте зарегистрируем новый ресурс, заменив маршрут posts на:

1
2
3
# app/routes.php
 
Route::resource(‘posts’, ‘PostsController’);

… и создать необходимый изобретательный контроллер с Artisan.

1
2
$ php artisan controller:make PostsController
Controller created successfully!

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
 
# app/controllers/PostsController.php
 
class PostsController extends BaseController {
 
  protected $post;
 
  public function __construct(Post $post)
  {
      $this->post = $post;
  }
 
  public function index()
  {
      $posts = $this->post->all();
 
      return View::make(‘posts.index’)
          ->with(‘posts’, $posts);
  }
 
}

Обратите внимание, что лучше напечатать интерфейс, а не ссылаться на саму модель Eloquent. Но по одному за раз! Давайте работать до этого.

Это значительно лучший способ структурировать код. Поскольку модель теперь внедряется, у нас есть возможность заменить ее проверенной версией для тестирования. Вот пример того, как это сделать:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
 
# app/tests/controllers/PostsControllerTest.php
 
class PostsControllerTest extends TestCase {
 
  public function __construct()
  {
      // We have no interest in testing Eloquent
      $this->mock = Mockery::mock(‘Eloquent’, ‘Post’);
  }
 
  public function tearDown()
  {
      Mockery::close();
  }
 
  public function testIndex()
  {
      $this->mock
           ->shouldReceive(‘all’)
           ->once()
           ->andReturn(‘foo’);
 
      $this->app->instance(‘Post’, $this->mock);
 
      $this->call(‘GET’, ‘posts’);
 
      $this->assertViewHas(‘posts’);
  }
 
}

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

1
2
3
$this->mock
    ->shouldReceive(‘all’)
    ->once();

К сожалению, если вы решите отказаться от написания кода для интерфейса и вместо этого внедрить модель Post в контроллер, придется использовать хитрость, чтобы обойти использование Eloquent статики, которая может конфликтовать с Mockery. Вот почему мы угоняем оба класса Post и Eloquent в конструкторе теста перед загрузкой официальных версий. Таким образом, у нас есть чистый лист, чтобы заявить о любых ожиданиях. Недостатком, конечно, является то, что мы не можем по умолчанию использовать любые существующие методы, используя методы Mockery, такие как makePartial() .

Контейнер Laravel IoC значительно облегчает процесс внедрения зависимостей в ваши классы. Каждый раз, когда запрашивается контроллер, он разрешается из контейнера IoC. Таким образом, когда нам нужно объявить, что для тестирования должна использоваться фиктивная версия Post , нам нужно предоставить Laravel только тот экземпляр Post который следует использовать.

1
$this->app->instance(‘Post’, $this->mock);

Думайте об этом коде как о « Эй, Ларавел, когда вам нужен экземпляр Post , я хочу, чтобы вы использовали мою ложную версию». Поскольку приложение расширяет Container , у нас есть доступ ко всем методам IoC непосредственно из него.

После создания экземпляра контроллера Laravel использует возможности PHP-отражения, чтобы прочитать подсказку и ввести зависимость для вас. Это верно; вам не нужно писать одну привязку, чтобы учесть это; это автоматизировано!


Другое распространенное ожидание, которое вы обнаружите при написании, — это то, которое гарантирует, что пользователь перенаправляется в нужное место, возможно, после добавления нового сообщения в базу данных. Как мы можем достичь этого?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
# app/tests/controllers/PostsControllerTest.php
 
public function testStore()
{
    $this->mock
         ->shouldReceive(‘create’)
         ->once();
 
    $this->app->instance(‘Post’, $this->mock);
 
    $this->call(‘POST’, ‘posts’);
 
    $this->assertRedirectedToRoute(‘posts.index’);
 
}

Предполагая, что мы следим за спокойным вкусом, чтобы добавить новый пост, мы добавили бы POST в коллекцию или posts (не путайте метод запроса POST с именем ресурса, которое просто имеет то же имя).

1
$this->call(‘POST’, ‘posts’);

Тогда нам нужно только использовать другое вспомогательное утверждение Laravel, assertRedirectedToRoute .

Совет: когда ресурс зарегистрирован в Laravel ( Route::resource() ), платформа автоматически зарегистрирует необходимые именованные маршруты. Запустите php artisan routes если вы когда-нибудь забудете, как эти имена

Вы также можете убедиться, что $_POST передается методу create . Несмотря на то, что мы физически не отправляем форму, мы можем разрешить это с помощью метода Input::replace() , который позволяет нам «заглушить» этот массив. Вот модифицированный тест, который использует метод Mockery with() для проверки аргументов, переданных методу, на который ссылается shouldReceive .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
# app/tests/controllers/PostsControllerTest.php
 
public function testStore()
{
    Input::replace($input = [‘title’ => ‘My Title’]);</p>
 
    $this->mock
         ->shouldReceive(‘create’)
         ->once()
         ->with($input);
 
    $this->app->instance(‘Post’, $this->mock);
 
    $this->call(‘POST’, ‘posts’);
 
    $this->assertRedirectedToRoute(‘posts.index’);
}

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

  1. Перенаправьте обратно в форму «Создать сообщение» и отобразите ошибки проверки формы.
  2. Перенаправить в коллекцию или именованный маршрут posts.index .

Рекомендуется, чтобы каждый тест представлял только один путь в вашем коде.

Этот первый путь будет для неудачной проверки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
# app/tests/controllers/PostsControllerTest.php
 
public function testStoreFails()
{
    // Set stage for a failed validation
    Input::replace([‘title’ => »]);
 
    $this->app->instance(‘Post’, $this->mock);
 
    $this->call(‘POST’, ‘posts’);
 
    // Failed validation should reload the create form
    $this->assertRedirectedToRoute(‘posts.create’);
 
    // The errors should be sent to the view
    $this->assertSessionHasErrors([‘title’]);
}

Приведенный выше фрагмент кода явно объявляет, какие ошибки должны существовать. В качестве альтернативы вы можете опустить аргумент assertSessionHasErrors , и в этом случае он просто проверит, что пакет сообщений был перепрошит (в переводе ваше перенаправление включает withErrors($errors) ).

Теперь для теста, который обрабатывает успешную проверку.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
# app/tests/controllers/PostsControllerTest.php
 
public function testStoreSuccess()
{
    // Set stage for successful validation
    Input::replace([‘title’ => ‘Foo Title’]);</p>
 
    $this->mock
         ->shouldReceive(‘create’)
         ->once();
 
    $this->app->instance(‘Post’, $this->mock);
 
    $this->call(‘POST’, ‘posts’);
 
    // Should redirect to collection, with a success flash message
    $this->assertRedirectedToRoute(‘posts.index’, [‘flash’]);
}

Рабочий код для этих двух тестов может выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/controllers/PostsController.php
 
public function store()
{
    $input = Input::all();
 
    // We’ll run validation in the controller for convenience
    // You should export this to the model, or a service
    $v = Validator::make($input, [‘title’ => ‘required’]);
 
    if ($v->fails())
    {
        return Redirect::route(‘posts.create’)
            ->withInput()
            ->withErrors($v->messages());
    }
 
    $this->post->create($input);
 
    return Redirect::route(‘posts.index’)
        ->with(‘flash’, ‘Your post has been created!’);
}

Обратите внимание, как Validator вложен непосредственно в контроллер? Как правило, я бы рекомендовал вам абстрагироваться от этой услуги. Таким образом, вы можете проверить свою проверку изолированно от любых контроллеров или маршрутов. Тем не менее, давайте для простоты оставим вещи такими, какие они есть. Нужно иметь в виду, что мы не издеваемся над Validator , хотя вы, конечно, можете это сделать. Поскольку этот класс является фасадом, он может быть легко заменен на shouldReceive версию с помощью метода shouldReceive в shouldReceive , без необходимости беспокоиться о внедрении экземпляра через конструктор. Выиграть!

1
2
3
4
5
# app/controllers/PostsController.php
 
Validator::shouldReceive(‘make’)
    ->once()
    ->andReturn(Mockery::mock([‘fails’ => ‘true’]));

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

1
Mockery::mock([‘fails’ => ‘true’])

подготовит объект, содержащий метод fails() который возвращает true .


Чтобы обеспечить оптимальную гибкость, вместо создания прямой связи между вашим контроллером и ORM, например, Eloquent, лучше кодировать интерфейс. Существенным преимуществом этого подхода является то, что, если вам, возможно, понадобится заменить Eloquent, например, для Mongo или Redis, для этого буквально требуется изменить одну строку. Более того, контроллер никогда не нужно трогать.

Хранилища представляют уровень доступа к данным вашего приложения.

Как может выглядеть интерфейс для управления уровнем базы данных Post ? Это должно начать вас.

01
02
03
04
05
06
07
08
09
10
11
12
13
<?php
 
# app/repositories/PostRepositoryInterface.php
 
interface PostRepositoryInterface {
 
    public function all();
 
    public function find($id);
 
    public function create($input);
 
}

Это, конечно, можно расширить, но мы добавили минимальные методы для демонстрации: all , find и create . Обратите внимание, что интерфейсы репозитория хранятся в app/repositories . Поскольку эта папка по умолчанию не загружается, нам нужно обновить файл composer.json чтобы приложение могло ссылаться на него.

1
2
3
4
5
6
7
8
// composer.json
 
«autoload»: {
  «classmap»: [
    // ….
    «app/repositories»
  ]
}

Когда в этот каталог добавляется новый класс, не забудьте composer dump-autoload -o . Флаг -o , ( оптимизировать ) является необязательным, но его всегда следует использовать, как лучший метод.

Если вы попытаетесь внедрить этот интерфейс в ваш контроллер, Laravel обрушится на вас. Преуспевать; попробуйте и посмотрите. Вот модифицированный PostController , который был обновлен для добавления интерфейса, а не модели Post Eloquent.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
 
# app/controllers/PostsController.php
 
use Repositories\PostRepositoryInterface as Post;
 
class PostsController extends BaseController {
 
    protected $post;
 
    public function __construct(Post $post)
    {
        $this->post = $post;
    }
 
    public function index()
    {
        $posts = $this->post->all();
 
        return View::make(‘posts.index’, [‘posts’ => $posts]);
    }
 
}

Если вы запустите сервер и просмотрите выходные данные, вы встретитесь со страшной (но красивой) страницей ошибок Whoops , заявив, что « PostRepositoryInterface не является экземпляром ».

Не подлежит

Если вы думаете об этом, конечно, рамки кричат! Ларавел умный, но он не умеет читать мысли. Необходимо указать, какая реализация интерфейса должна использоваться в контроллере.

А пока давайте добавим эту привязку в app/routes.php . Позже мы вместо этого будем использовать поставщиков услуг для хранения подобной логики.

1
2
3
4
5
6
# app/routes.php
 
App::bind(
    ‘Repositories\PostRepositoryInterface’,
    ‘Repositories\EloquentPostRepository’
);

Вербализируйте этот вызов функции как « Laravel, детка, когда тебе нужен экземпляр PostRepositoryInterface , я хочу, чтобы ты использовал EloquentPostRepository ».

app/repositories/EloquentPostRepository будет просто оболочкой для Eloquent, реализующей PostRepositoryInterface . Таким образом, мы не ограничиваем API (и любую другую реализацию) интерпретацией Eloquent; мы можем назвать методы так, как хотим.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php namespace Repositories;
 
# app/repositories/EloquentPostRepository.php
 
use Repositories\PostRepositoryInterface;
use Post;
 
class EloquentPostRepository implements PostRepositoryInterface {
 
  public function all()
  {
      return Post::all();
  }
 
  public function find($id)
  {
      return Post::find($id);
  }
 
  public function create($input)
  {
      return Post::create($input);
  }
 
}

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

Это все, что нужно! Обновите браузер, и все должно вернуться к нормальной жизни. Только теперь ваше приложение намного лучше структурировано, и контроллер больше не связан с Eloquent.

Давайте представим, что через несколько месяцев ваш босс сообщит вам, что вам нужно поменять Eloquent с Redis. Итак, поскольку вы структурировали свое приложение таким образом, чтобы обеспечить будущее, вам нужно только создать новую реализацию app/repositories/RedisPostRepository :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php namespace Repositories;
 
# app/repositories/RedisPostRepository.php
 
use Repositories\PostRepositoryInterface;
 
class RedisPostRepository implements PostRepositoryInterface {
 
  public function all()
  {
      // return all with Redis
  }
 
  public function find($id)
  {
      // return find one with Redis
  }
 
  public function create($input)
  {
      // return create with Redis
  }
 
}

И обновить привязку:

1
2
3
4
5
6
# app/routes.php
 
App::bind(
    ‘Repositories\PostRepositoryInterface’,
    ‘Repositories\RedisPostRepository’
);

Мгновенно вы теперь используете Redis в своем контроллере. Обратите внимание, как никогда не трогали app/controllers/PostsController.php ? В этом вся прелесть!


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

В последнем разделе этой статьи мы будем PSR-ify наш код и использовать поставщиков услуг для регистрации любых применимых привязок.

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

Загрузчик PSR-0 может быть зарегистрирован в Composer через объект psr-0 .

1
2
3
4
5
6
7
// composer.json
 
«autoload»: {
    «psr-0»: {
        «Way»: «app/lib/»
    }
}

Поначалу синтаксис может сбивать с толку. Это конечно было для меня. Простой способ расшифровать "Way": "app/lib/" — подумать про себя: « Базовая папка для пространства имен Way находится в app/lib . » Конечно, замените мою фамилию именем вашего проекта. , Структура каталогов, которая будет соответствовать этому:

  • приложение/
    • Библиотека /
    • Путь/

Далее, вместо того, чтобы группировать все репозитории в каталог repositories , более элегантный подход может состоять в том, чтобы разделить их на несколько каталогов, например:

  • приложение/
    • Библиотека /
    • Путь/
      • Место хранения/
      • Почта/
        • PostRepositoryInterface.php
        • EloquentPostRepository.php

Крайне важно придерживаться этого соглашения об именах и папках, если мы хотим, чтобы автозагрузка работала так, как ожидалось. Осталось только обновить пространства имен для PostRepositoryInterface и EloquentPostRepository .

01
02
03
04
05
06
07
08
09
10
11
12
13
<?php namespace Way\Storage\Post;
 
# app/lib/Way/Storage/Post/PostRepositoryInterface.php
 
interface PostRepositoryInterface {
 
    public function all();
 
    public function find($id);
 
    public function create($input);
 
}

И для реализации:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php namespace Way\Storage\Post;
 
# app/lib/Way/Storage/Post/EloquentPostRepository.php
 
use Post;
 
class EloquentPostRepository implements PostRepositoryInterface {
 
    public function all()
    {
        return Post::all();
    }
 
    public function find($id)
    {
        return Post::find($id);
    }
 
    public function create($input)
    {
        return Post::create($input);
    }
 
}

Там мы идем; это намного чище. Но как насчет этих надоедливых привязок? Файл маршрутов может быть удобным местом для экспериментов, но нет смысла хранить их там постоянно. Вместо этого мы будем использовать поставщиков услуг.

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

register() поставщика услуг register() будет запущен автоматически Laravel.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?php namespace Way\Storage;
 
# app/lib/Way/Storage/StorageServiceProvider.php
 
use Illuminate\Support\ServiceProvider;
 
class StorageServiceProvider extends ServiceProvider {
 
    // Triggered automatically by Laravel
    public function register()
    {
        $this->app->bind(
            ‘Way\Storage\Post\PostRepositoryInterface’,
            ‘Way\Storage\Post\EloquentPostRepository’
        );
    }
 
}

Чтобы сделать этот файл известным Laravel, вам нужно всего лишь включить его в app/config/app.php , в массиве providers .

1
2
3
4
5
6
7
8
# app/config/app.php
 
‘providers’ => array(
    ‘Illuminate\Foundation\Providers\ArtisanServiceProvider’,
    ‘Illuminate\Auth\AuthServiceProvider’,
    // …
    ‘Way\Storage\StorageServiceProvider’
)

Хорошо; Теперь у нас есть специальный файл для регистрации новых привязок.

С нашей новой структурой вместо того, чтобы издеваться над моделью Eloquent, мы можем вместо этого PostRepositoryInterface . Вот пример одного такого теста:

01
02
03
04
05
06
07
08
09
10
11
12
13
# app/tests/controllers/PostsControllerTest.php
 
public function testIndex()
{
    $mock = Mockery::mock(‘Way\Storage\Post\PostRepositoryInterface’);
    $mock->shouldReceive(‘all’)->once();
 
    $this->app->instance(‘Way\Storage\Post\PostRepositoryInterface’, $mock);
 
    $this->call(‘GET’, ‘posts’);
 
    $this->assertViewHas(‘posts’);
}

Тем не менее, мы можем улучшить это. PostsControllerTest собой PostsControllerTest что каждый метод в PostsControllerTest потребует PostsControllerTest версии хранилища. Поэтому лучше подготовить часть этой подготовительной работы в свой собственный метод, например так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# app/tests/controllers/PostsControllerTest.php
 
public function setUp()
{
    parent::setUp();
 
    $this->mock(‘Way\Storage\Post\PostRepositoryInterface’);
}
 
public function mock($class)
{
    $mock = Mockery::mock($class);
 
    $this->app->instance($class, $mock);
 
    return $mock;
}
 
public function testIndex()
{
    $this->mock->shouldReceive(‘all’)->once();
 
    $this->call(‘GET’, ‘posts’);
 
    $this->assertViewHas(‘posts’);
}

Не плохо, а?

Теперь, если вы хотите быть супер-мухой и хотите добавить немного тестовой логики в свой производственный код, вы можете даже выполнить макет в модели Eloquent! Это позволило бы:

1
Post::shouldReceive(‘all’)->once();

За кулисами это будет издеваться над PostRepositoryInterface и обновлять привязку IoC. Вы не можете стать намного более читабельным, чем это!

BaseModel этот синтаксис, требуется только обновить модель Post или, что лучше, BaseModel которую BaseModel все модели Eloquent. Вот пример первого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?php
 
# app/models/Post.php
 
class Post extends Eloquent {
 
    public static function shouldReceive()
    {
        $class = get_called_class();
        $repo = «Way\\Storage\\{$class}\\{$class}RepositoryInterface»;
        $mock = Mockery::mock($repo);
 
        App::instance($repo, $mock);
 
        return call_user_func_array([$mock, ‘shouldReceive’], func_get_args());
    }
 
}

Если вы сможете управлять внутренним сражением « Должен ли я встраивать логику тестирования в производственный код », вы обнаружите, что это позволяет значительно более удобочитаемые тесты.

01
02
03
04
05
06
07
08
09
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
<?php
 
# app/tests/controllers/PostsControllerTest.php
 
class PostsControllerTest extends TestCase {
 
    public function tearDown()
    {
        Mockery::close();
    }
 
    public function testIndex()
    {
        Post::shouldReceive(‘all’)->once();
 
        $this->call(‘GET’, ‘posts’);
 
        $this->assertViewHas(‘posts’);
    }
 
    public function testStoreFails()
    {
        Input::replace($input = [‘title’ => »]);
 
        $this->call(‘POST’, ‘posts’);
 
        $this->assertRedirectedToRoute(‘posts.create’);
        $this->assertSessionHasErrors();
    }
 
    public function testStoreSuccess()
    {
        Input::replace($input = [‘title’ => ‘Foo Title’]);
 
        Post::shouldReceive(‘create’)->once();
 
        $this->call(‘POST’, ‘posts’);
 
        $this->assertRedirectedToRoute(‘posts.index’, [‘flash’]);
    }
 
}

Это хорошо, не так ли? Надеюсь, эта статья не была слишком подавляющей. Главное — научиться организовывать свои репозитории таким образом, чтобы их было как можно проще высмеивать и внедрять в контроллеры. В результате этих усилий ваши тесты будут молниеносно!

Эта статья является отрывком из моей будущей книги Laravel Testing Decoded . Следите за его выпуском в мае 2013 года!