二、自动化测试——迁移和植入您的数据库

到目前为止,我们已经创建了一些基本模型和数据库的概要。现在,我们需要创建数据库迁移和播种。传统上,数据库“转储”文件被用作传递模式和数据的一种方式,模式是表的结构,数据是初始或预定义的记录,如默认值;不变的列表,如城市或国家;和“管理员”等用户。这些包含 SQL 的转储文件可以提交给源代码控制。这并不总是维护数据库完整性的最佳方式;因为每次开发人员添加记录或修改数据库时,团队中的所有开发人员都需要删除并重新创建数据库,或者手动添加或删除数据、表、行、列或索引。迁移允许数据库以代码的形式存在,实际驻留在 Laravel 项目中,并在源代码控制中进行版本控制。

迁移是从命令行运行的,如果数据库尚不存在,也可以在需要时自动创建数据库,或者删除并重新创建表,并填充已经存在的表。迁移在 Laravel 中已经存在了一段时间,所以它们出现在 Laravel 5 中并不奇怪。

利用拉威尔的迁移特征

第一步是运行artisan命令:

$ php artisan migrate:install

这将创建一个名为migration的表,该表有两列:migration,这是 MySQL 中的 varchar 255,以及batch,这是一个整数。Laravel 将使用此表来跟踪已运行的迁移。换句话说,它维护所有已经执行的操作的历史。以下是主要操作的列表:

  • install:如前所述,本次操作安装
  • refresh:此操作重置并重新运行所有迁移
  • reset:此操作回滚所有迁移
  • rollback:这个操作是“撤销”的一种,只是回滚上一个操作
  • status:这个操作产生一个类似于表格的迁移输出,并说明它们是否已经运行

迁徙的一个例子

Laravel 5 在/database/migrations目录中包含两个迁移。

第一次迁移创建users表。

第二个创建password_resets表,正如您可能已经猜到的,该表用于恢复丢失的密码。除非另有规定,迁移在/config/database.php配置文件中配置的数据库上运行:

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration {

  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up()
  {
    Schema::create('users', function(Blueprint $table)
    {
      $table->smallIncrements('id')->unsigned();
      $table->string('name');
      $table->string('email')->unique();
      $table->string('password', 60);
      $table->rememberToken();
      $table->timestamps();
      $table->softDeletes();
    });
  }

  /**
   * Reverse the migrations.
   *
   * @return void
   */
  public function down()
  {
    Schema::drop('users');
  }

}

迁移扩展Migration类并使用Blueprint类。

有两种方法:updown,分别在使用migrate命令和rollback命令时使用。调用Schema::create()方法时,表名是第一个参数,函数回调是第二个参数,它接受一个Blueprint对象的实例作为参数。

创建表格

$table对象有几个方法可以执行创建索引、设置自动递增字段、说明应该创建哪种类型的字段以及将字段名称作为参数传递等任务。

第一个命令用于创建一个自动增量字段id,它将是表的主键。然后,创建字符串字段,如nameemailpassword。请注意,唯一的方法链接到email字段的create语句,声明email字段将用作登录名/用户标识,因为这是大多数现代网络应用中的常见做法。rememberToken用于允许用户在每次会话中保持认证。该令牌在每次登录和注销时都会重置,从而保护用户免受潜在的恶意劫持。

拉弗尔的迁徙魔法

Laravel 迁移还能够创建时间戳字段,这些字段用于通过表行自动存储每个模型的创建和更新信息。

$table- >时间戳();

下面的代码行告诉迁移在表中自动创建两列,即created_atupdated_at,这是由 Laravel 的雄辩 对象关系映射 ( ORM )自动使用的,以允许应用知道对象是何时创建的以及何时更新的:

$table->timestamps()

在以下示例中,字段更新如下:

/*
*   created_at is set with timestamps
*/
$user = new User();
$user->email = "johndoe@acmewidgets.com";
$user->name = "John Doe";
$user->save(); // created_at is set with timestamps

/*
*   updated_at is set with timestamps
*/
$user = User::find(1); //where 1 is the $id
$user->email = "johndoe@acmeenterprise.com";
$user->save(); //updated_at is updated

另一个很棒的 Laravel 特性是软删除字段。这提供了一种类型的回收站 ,允许数据在以后任意恢复。

该特性只是向表中添加另一列,以允许数据的软删除。要添加到迁移中的代码如下所示:

$table->softDeletes();

这将向database, deleted_at,添加一列,该列或者将null作为其值,或者将时间戳作为记录被删除的时间。这将在您的数据库应用中构建回收站功能。

运行以下命令:

$ php artisan migrate

迁移开始并创建表。现在出现迁移表,如下图截图所示:

$table->timestamps();

users表的结构如下截图所示:

$table->timestamps();

要回滚迁移,请运行以下命令:

$ php artisan migrate:rollback

rollback 命令使用迁移表来确定回滚哪些操作。在这种情况下,migrations表在运行后,现在是空的。

从模式到迁移

开发过程中常见的情况是创建了一个模式,然后我们需要从该模式创建一个迁移。在撰写本文时,在 Laravel 内核中还没有官方工具可以做到这一点,但是有几个包可供使用。

其中一个包就是migrations-generator包。

首先,在composer.json文件的require-dev部分添加以下行,以要求composer.json文件中的migrations-generator依赖项:

"require-dev": {
    "phpunit/phpunit": "~4.0",
    "phpspec/phpspec": "~2.1",
    "xethron/migrations-generator": "dev-feature/laravel-five-stable",
    "way/generators": "dev-feature/laravel-five-stable"
  },

还需要在根级别将以下文本添加到composer.json文件中:

"repositories": [
  {
    "type": "git",
    "url": "git@github.com:jamisonvalenta/Laravel-4-Generators.git"
  }],

作曲家的 require-dev 命令

require相反, require-dev命令是一种允许仅在开发阶段需要的某些包的编写器机制。大多数测试工具和迁移工具将只在本地开发机器、质量保证机器和/或持续集成环境中使用,而不会在生产环境中使用。这种机制使您的生产安装没有不必要的软件包。

Laravel 的提供商阵列

Laravel 在config/app.php文件中的providers数组列出了 Laravel 在任何时候都可以使用的提供商。

我们将添加way generatorXethron migration服务提供商:

'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
          Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
          Illuminate\Auth\AuthServiceProvider::class,
          Illuminate\Broadcasting\BroadcastServiceProvider::class,
        ...
    'Way\Generators\GeneratorsServiceProvider',
    'Xethron\MigrationsGenerator\MigrationsGeneratorServiceProvider'
]

作曲家更新命令

composer update命令是一种简单而强大的方法,可以确保所有需要到位的东西都能够正常工作并且没有错误。运行此命令后,我们现在可以运行迁移了。

生成迁移

只需输入以下命令:

$ php artisan

artisan命令将显示所有可能命令的列表。migrate:generate命令应包含在有效命令列表中。如果此命令不在列表中,则说明某些内容配置不正确。

一旦确认列表中存在migrate:generate命令,只需运行以下命令:

$ php artisan migrate:generate

这将启动该过程。

在这个例子中,我们使用了 MySQL 数据库。当出现提示时,通过输入Y,该过程将开始,并且输出应该显示为数据库中的每个表创建的一个迁移文件。

您的命令提示符应该以这种方式出现在最后:

Using connection: mysql

Generating migrations for: accommodations, amenities, amenity_room, cities, countries, currencies, locations, rates, reservation_room, reservations, rooms, states, users
Do you want to log these migrations in the migrations table? [Y/n] Y
Migration table created successfully.
Next Batch Number is: 1\. We recommend using Batch Number 0 so that it becomes the "first" migration [Default: 0] 
Setting up Tables and Index Migrations
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_accommodations_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_amenities_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_amenity_room_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_cities_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_countries_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_currencies_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_locations_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_rates_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_reservation_room_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_reservations_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_rooms_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_states_table.php
Created: /var/www/laravel.example/database/migrations/2015_02_07_170311_create_users_table.php

Finished!

迁移解剖

考虑迁移文件中某一行的示例;我们可以看到 table 对象被用在一系列方法中。迁移文件的下一行在locations表的位置有效属性中设置状态属性:

$table->smallInteger('state_id')->unsigned()->index('state_id');

列表表格

通常,需要创建或导入通常保持不变的有限项目列表,如城市、州、国家和类似项目。让我们称这些列表或查找表。在这些表中,标识通常应该是正数。这些列表可能会增加,但通常不会有任何被删除或更新的数据。smallInteger类型用于保持表小,也表示属于有限列表的值,这种东西不会自然增长。下一种方法unsigned规定上限为 65535。该值应该足以代表酒店所在的大多数州、省或类似类型的地理区域。链中的最后一个方法向数据库列添加索引。这在像这样的列表中非常重要,这些列表用于select语句或read语句。Read声明将在第 9 章缩放 Laravel 中讨论。使用无符号是很重要的,因为它使正极限加倍,否则正极限将是 32767。使用索引,我们可以加快查找时间,并访问表中数据的缓存版本。

软删除和时间戳属性

关于列表表的softDeletes``timestamps,看情况而定。如果表不是很大,跟踪任何更新、插入或删除(如果发生的话)应该不会太有害;然而,如果列表中包含的国家很少发生变化,而且变化很小,那么谨慎的做法是省略softDeletestimestamps。因此,整个表可能会放入内存,速度会非常快。要省略时间戳,需要添加以下代码行:

public $timestamps = false;

创造种子

为了创建我们的数据库播种器,我们将修改扩展SeederDatabaseSeeder类。文件的名字是database/seeds/DatabaseSeeder.php。文件的内容如下:

<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder {

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();

        //create a user
        $user = new \MyCompany\User();
        $user->id=1;
        $user->email = "testing@tester.com";
        $user->password = Hash::make('p@ssw0rd');
        $user->save();

        //create a country
        $country = new \MyCompany\Accommodation\Location\State;
        $country->name = "United States";
        $country->id = 236;
        $country->save();

        //create a state
        $state = new \MyCompany\Accommodation\Location\State;
        $state->name = "Pennsylvania";
        $state->id = 1;
        $state->save();

        //create a city
        $city = new \MyCompany\Accommodation\Location\City;
        $city->name = "Pittsburgh";
        $city->save();

        //create a location
        $location = new \MyCompany\Accommodation\Location;
        $location->city_id = $city->id;
        $location->state_id = $state->id;
        $location->country_id = 236;
        $location->latitude = 40.44;
        $location->longitude = 80;
        $location->code = '15212';
        $location->address_1 = "100 Main Street";
        $location->save();

        //create a new accommodation
        $accommodation = new \MyCompany\Accommodation;
        $accommodation->name = "Royal Plaza Hotel";
        $accommodation->location_id = $location;
        $accommodation->description = "A modern, 4-star hotel";
        $accommodation->save();

        //create a room
        $room1 = new \MyCompany\Accommodation\Room;
        $room1->room_number= 'A01';
        $room1->accommodation_id = $accommodation->id;
        $room1->save();

        //create another room
        $room2 = new \MyCompany\Accommodation\Room;
        $room2->room_number= 'A02';
        $room2->accommodation_id = $accommodation->id;
        $room2->save();

        //create the room array
        $rooms = [$room1,$room2];

    }

}

种子文件设置了非常基本的可能场景。对于初始测试,我们甚至不需要将每个可能的国家、州、城市和位置添加到数据库中;我们只需要添加必要的信息来创建各种场景。创建新的预订;例如,我们将为每个用户、国家、州、城市、位置和住宿模型创建一个实例,然后创建两个房间,并将其添加到房间数组中。

让我们为预订创建一个存储库,它将实现一个非常简单的存储库界面:

<?php

namespace MyCompany\Accommodation;

interface RepositoryInterface {
    public function create($attributes);
}

现在我们来创建ReservationRepository,实现RepositoryInterface:

<?php

namespace MyCompany\Accommodation;

class ReservationRepository implements RepositoryInterface {
    private $reservation;

    function __construct($reservation)
    {
        $this->reservation = $reservation;
    }

    public function create($attributes)
    {
        $this->reservation->create($attributes);
        return $this->reservation;
    }
}

现在,我们将创建创建预订和填充数据透视表所需的方法reservation_room:

public function create($attributes)
{

    $modelAttributes= array_except($attributes, ['rooms']);

    $reservation = $this->reservationModel->create($modelAttributes);
    if (isset($attributes['rooms']) ) {
        $reservation->rooms()->sync($attributes['rooms']);
    }
    return $reservation;
}

类型

array_except() Laravel 助手用于返回attributes数组,但$rooms数组除外,该数组将用于sync()功能。

这里,我们将模型的每个属性设置为方法中设置的属性。我们需要添加在预订和房间之间建立多对多关系的方法:

public function rooms(){
    return $this->belongsToMany('MyCompany\Accommodation\Room')->withTimestamps();
}

在这种情况下,我们需要将withTimestamps()添加到关系中,以便更新时间戳,指示关系何时保存在reservation_room透视表中。

用 PHPUnit 进行数据库测试

PHPUnit 与 Laravel 5 的集成很好,就像与 Laravel 4 的集成一样,所以设置测试环境相当容易。一个好的测试方法是使用 SQLite 数据库并将其设置为驻留在内存中,但是您需要修改config/database.php文件,如下所示:

    'default' => 'sqlite',
       'connections' => array(
        'sqlite' => array(
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => '',
        ),
    ),

然后,我们需要修改phpunit.xml文件来设置一个DB_DRIVER环境变量:

<php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="DB_DRIVER" value="sqlite"/>
</php>

然后,我们需要修改config/database.php文件中的以下一行:

'default' => 'mysql',

我们修改前面的行以匹配下面的行:

'default' => env('DB_DRIVER', 'mysql'),

现在,我们将设置 PHPUnit 在内存中的sqlite数据库上运行我们的迁移。

tests目录中,有两个类:扩展LaravelTestCase类的TestCase类和扩展TestCase类的ExampleTest类。

我们需要向TestCase添加两种方法来执行迁移,运行播种器,然后将数据库恢复到其原始状态:

<?php

class TestCase extends Illuminate\Foundation\Testing\TestCase {

    public function setUp()
    {
        parent::setUp();
        Artisan::call('migrate');
        Artisan::call('db:seed');
    }

    /**
    * Creates the application.
    *
    * @return \Illuminate\Foundation\Application
    */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';
        $app->make('Illuminate\Contracts\Console\Kernel')->bootstrap();
        return $app;
    }

    public function tearDown()
    {
        Artisan::call('migrate:rollback');
    }
}

现在,我们将创建一个 PHPUnit 测试来验证数据是否正确保存在数据库中。我们需要将tests/ExampleTest.php修改为以下代码:

<?php

class ExampleTest extends TestCase {

    /**
    * A basic functional test example.
    *
    * @return void
    */

public function testReserveRoomExample()
    {

        $reservationRepository = new \MyCompany\Accommodation\ReservationRepository(
            new \MyCompany\Accommodation\Reservation());
        $reservationValidator = new \MyCompany\Accommodation\ReservationValidator();
        $start_date = '2015-10-01';
        $end_date = '2015-10-10';
        $rooms = \MyCompany\Accommodation\Room::take(2)->lists('id')->toArray();
        if ($reservationValidator->validate($start_date,$end_date,$rooms)) {
            $reservation = $reservationRepository->create(['date_start'=>$start_date,'date_end'=>$end_date,'rooms'=>$rooms,'reservation_number'=>'0001']);
        }

        $this->assertInstanceOf('\MyCompany\Accommodation\Reservation',$reservation);
        $this->assertEquals('2015-10-01',$reservation->date_start);
        $this->assertEquals(2,count($reservation->rooms));
}

运行 PHPUnit

要启动 PHPUnit,只需键入以下命令:

$ phpunit

测试将会运行。由于Reservation类的create方法返回一个预订,我们可以使用 PHPUnit 的assertInstanceOf方法来确定数据库中是否创建了一个预订。我们可以添加任何其他断言,以确保保存的值正是我们想要的。例如,我们可以断言开始日期等于'2015-10-01'room数组的大小等于two。与testBasicExample()方法一起,我们可以确保对"/"GET请求返回一个200。PHPUnit 结果如下所示:

Running PHPUnit

请注意,有两个点代表测试。 OK 表示没有失败,再次告诉我们有两个测试,四个断言;示例中的一个断言和其他三个断言,我们已经将其添加到我们的testReserveRoomExample测试中。如果我们测试的是三个房间而不是两个,PHPUnit 会产生以下输出:

$ phpunit
PHPUnit 4.5.0 by Sebastian Bergmann and contributors.

Configuration read from /var/www/laravel.example/phpunit.xml

.
F

Time: 1.59 seconds, Memory: 10.75Mb

There was 1 failure:

1) ExampleTest::testReserveRoomExample
Failed asserting that 2 matches expected 3.

/var/www/laravel.example/tests/ExampleTest.php:24

FAILURES! 
Tests: 2, Assertions: 4, Failures: 1.

注意不是第二个点,我们有一个F代表失败,而不是OK,我们被告知有1失败。PHPUnit 然后列出了哪些测试失败了,并很好地告诉我们我故意修改为不正确的那一行,如下所示:

   $this->assertEquals(3,count($reservationResult->rooms));

前一行确实不正确:

Failed asserting that 2 matches expected 3.

记住2是计数($reservationResult->rooms)的值。

使用 Behat 进行功能测试

虽然 phpspec 遵循规范的 BDD,并且对于隔离的规范和设计很有用,但是它的补充工具 Behat 用于集成和功能测试。由于 phpspec 建议模拟所有的事情,数据库查询实际上不会被执行,因为数据库不在该方法的上下文中。Behat 是对某个特性进行行为测试的一个很好的工具。虽然 phpspec 已经包含在 Laravel 5 的依赖项中,但 Behat 将作为外部模块安装。

应该运行以下命令来安装 Behat 并使其与 Laravel 5 一起工作:

$ composer require behat/behat behat/mink behat/mink-extension laracasts/behat-laravel-extension --dev

运行作曲家更新后,Behat 的功能被添加到 Laravel 中。接下来,应该在 Laravel 项目的根目录下添加一个behat.yaml文件,以指定要使用的扩展名。

接下来,运行以下命令:

$ behat --init

这将创建一个内部有bootstrap目录的features目录。一个FeaturesContext班将也将被创建。每次运行behat时,引导程序中的所有内容都会运行。这对于自动运行迁移和播种非常有用。

features/bootstrap/FeaturesContext.php文件是这样的:

<?php

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context, SnippetAcceptingContext
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
    }
}

接下来FeatureContext类需要扩展MinkContext类,因此类定义行需要修改如下:

class FeatureContext implements Context, SnippetAcceptingContext

接下来, preparecleanup方法将被添加到类中,以便执行迁移。我们将添加@BeforeSuite@AfterSuite注释,告诉 Behat 在每个套件之前执行迁移和播种,并迁移到回滚,以便在每个套件之后将数据库恢复到其原始状态。在文档块中使用注释将在第 6 章用注释驯服复杂性中讨论。我们现在的课堂结构如下:

<?php

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context, SnippetAcceptingContext
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    public function __construct()
    {
    }
     /**
     * @BeforeSuite
     */
     public static function prepare(SuiteEvent $event)
     {
        Artisan::call('migrate');
        Artisan::call('db:seed');

     }

     /**
     * @AfterSuite 
     */
     public function cleanup(ScenarioEvent $event)
     {
        Artisan::call('migrate:rollback');
     }
}

现在,需要创建一个特征文件。在房间目录中创建reservation.feature:

Feature: Reserve Room
  In order to verify the reservation system
  As an accommodation reservation user
  I need to be able to create a reservation in the system
  Scenario: Reserve a Room
   When I create a reservation
         Then I should have one reservation

behat运行如下:

$ behat

产生以下输出:

Feature: Reserve Room
 In order to verify the reservation system
 As an accommodation reservation user
 I need to be able to create a reservation in the system

 Scenario: List 2 files in a directory # features/reservation.feature:5
 When I create a reservation
 Then I should have one reservation

1 scenario (1 undefined)
2 steps (2 undefined)
0m0.10s (7.48Mb)

--- FeatureContext has missing steps. Define them with these snippets:

 /**
 * @When I create a reservation
 */
 public function iCreateAReservation()
 {
 throw new PendingException();
 }

 /**
 * @Then I should have one reservation
 */
 public function iShouldHaveOneReservation()
 {
 throw new PendingException();
 }

Behat,就像 phpspec 一样,熟练地产生输出,向你展示需要创建的方法。请注意,使用的是骆驼纹而不是蛇纹。这个代码应该复制到FeatureContext类中。请注意,默认情况下,会引发异常。

在这里,将调用 RESTful API,因此需要向项目中添加大量的 HTTP 包:

$ composer require guzzlehttp/guzzle

接下来,给类添加一个属性来保存guzzle对象。我们将向 RESTful 资源控制器添加一个POST请求,以创建一个预订并期待一个 201 代码。请注意,返回代码是一个字符串,需要转换为整数。接下来,执行get以返回所有预订。

应该只创建一个保留,因为每次都会运行迁移和播种:

<?php

use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Testwork\Hook\Scope\BeforeSuiteScope;
use Behat\Testwork\Hook\Scope\AfterSuiteScope;
use GuzzleHttp\Client;

/**
 * Defines application features from the specific context.
 */
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
    /**
     * Initializes context.
     *
     * Every scenario gets its own context instance.
     * You can also pass arbitrary arguments to the
     * context constructor through behat.yml.
     */
    protected $httpClient;

    public function __construct()
    {
        $this->httpClient = new Client();
    }
    /**
     * @BeforeSuite
     */
    public static function prepare(BeforeSuiteScope $scope)
    {
        Artisan::call('migrate');
        Artisan::call('db:seed');

    }

    /**
     * @When I create a reservation
     */
    public function iCreateAReservation()
    {
        $request = $this->httpClient->post('http://laravel.example/reservations',['body'=> ['start_date'=>'2015-04-01','end_date'=>'2015-04-04','rooms[]'=>'100']]);
        if ((int)$request->getStatusCode()!==201)
        {
            throw new Exception('A successfully created status code must be returned');
        }
    }

    /**
     * @Then I should have one reservation
     */
    public function iShouldHaveOneReservation()
    {
        $request = $this->httpClient->get('http://laravel.example/reservations');
        $arr = json_decode($request->getBody());
        if (count($arr)!==1)
        {
            throw new Exception('there must be exactly one reservation');
        }
    }

    /**
     * @AfterSuite
     */
    public static function cleanup(AfterSuiteScope $scope)
    {
        Artisan::call('migrate:rollback');
    }
}

    /**
     * @When I create a reservation
     */
    public function iCreateAReservation()
    {
        $request = $this->httpClient->post('http://laravel.example/reservations',['body'=> ['start_date'=>'2015-04-01','end_date'=>'2015-04-04','rooms[]'=>'100']]);
        if ((int)$request->getStatusCode()!==201)
        {
            throw new Exception('A successfully created status code must be returned');
        }
    }

现在,要创建,请使用来自命令行的工匠:

$ php artisan make:controller ReservationsController

以下是预约控制器的内容:

<?php namespace MyCompany\Http\Controllers;

use MyCompany\Http\Requests;
use MyCompany\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use MyCompany\Accommodation\ReservationRepository;
use MyCompany\Accommodation\ReservationValidator;
use MyCompany\Accommodation\Reservation;

class ReservationsController extends Controller {

    /**
    * Display a listing of the resource.
    *
    * @return Response
    */
    public function index()
    {
        return Reservation::all();
    }

    /**
    * Store a newly created resource in storage.
    *
    * @return Response
    */
    public function store()
    {
        $reservationRepository = new ReservationRepository(new Reservation());
        $reservationValidator = new ReservationValidator();
        if ($reservationValidator->validate(\Input::get('start_date'),
        \Input::get('end_date'),\Input::get('rooms')))
        {
        $reservationRepository->create(['date_start'=>\Input::get('start_date'),'date_end'=>\Input::get('end_date'),'rooms'=>\Input::get('rooms')]);
        return response('', '201');
        }
    }
}

最后,将ReservationController添加到位于app/Http/routes.php的的routes.php文件中:

Route::resource('reservations','ReservationController');

现在behat运行时,结果如下:

Feature: Reserve Room
 In order to verify the reservation system
 As an accommodation reservation user
 I need to be able to create a reservation in the system

 Scenario: Reserve a Room
 When I create a reservation         # FeatureContext::iCreateAReservation()
 Then I should have one reservation  # FeatureContext::iShouldHaveOneReservation()

1 scenario (1 passed)
2 steps (2 passed)

总结

配置 Laravel 从现有模式创建迁移文件对于非绿地项目来说也是一个有用的框架。通过在测试环境中运行迁移和播种,每个测试都可以从数据库的完全干净的版本以及初始数据中受益,初始数据允许它在数据库中“刚好足够”来最低限度地验证软件是否按需要运行。当遗留代码需要移植到 Laravel 时,PHPUnit 可以用来测试任何现有的函数。Behat 提供了一种基于行为的替代方案,可以巧妙地执行端到端测试。

我们在一个孤立的环境中使用 phpspec 设计了我们的类,只关注业务规则和客户的请求,同时嘲笑一些东西,比如实际的实体,比如房间。然后,我们通过使用功能测试工具 PHPUnit 来验证实际查询是否被正确执行并保存在数据库中。最后,我们使用 Behat 来执行端到端测试。

在下一章中,我们将看到 RESTful API 的创建,基本的 CRUD 操作(创建、读取、更新和删除),并讨论一些最佳实践。