[译] Laravel 5 之美 - 单元测试

原文地址: Laravel 5.1 Beauty - Testing

Note 本系列第四节内容.

本章会创建一个以后可以用到的项目便于以后我们的课程使用, 同时也会查课各种测试选项. 以后一段时间内会开发一个 Markdown 文本转换成 Html 的服务信息.

创建 l5beauty 项目

根据之前的章节 Six Steps to Starting a New Laravel 5.1 Project 创建 l5beauty 项目下面所示项目

首先在你的系统安装 app 框架

Step 1 - 安装项目框架

$ laravel new l5beauty
Crafting application...
Generating optimized class loader
Compiling common classes
Application key [rzUhyDksVxzTXFjzFYiOWToqpunI2m6X] set successfully.
Application ready! Build something amazing.

接下来设置 l5beauty.app 作为虚拟主机.

Step 2 - 配置服务器

$ serve l5beauty.app ~/l5beauty/public
dos2unix: converting file /vagrant/scripts/serve.sh to Unix format ...
 * Restarting nginx nginx                                                [ OK ]
php5-fpm stop/waiting
php5-fpm start/running, process 2169

回到主机, 添加以下记录到映射文件

Step 3 - 添加 l5beauty.app 到映射文件

192.168.10.10  l5beauty.app

从主机中, 按照以下步骤安装 NPM 本地包

Step 4 - NPM 本地安装

$ cd l5beauty
$ npm install
|
> [email protected] install /Users/chuck/Code/l5beauty/node_modules/laravel-\
    elixir/node_modules/gulp-sass/node_modules/node-sass
> node scripts/install.js

> [email protected] postinstall /Users/chuck/Code/l5beauty/node_modules/\
    laravel-elixir/node_modules/gulp-sass/node_modules/node-sass
> node scripts/build.js

`darwin-x64-node-0.10` exists; testing
Binary is fine; exiting
[email protected] node_modules/gulp
├── [email protected]
├── [email protected]

[snip]

回到主机, 创建数据库

Step 5 - 创建应用数据库

$ mysql --user=homestead --password=secret
mysql> create database l5beauty;
Query OK, 1 row affected (0.00 sec)

mysql> exit;
Bye

编辑 .env 文件, 修改数据库为 l5beauty.

Step 6 - 更改配置文件中的 DB_NAME

// Change the following line
DB_DATABASE=homestead

// To the correct value
DB_DATABASE=l5beauty

最后, 访问 http://l5beauty.app , 确保一切可用.

Figure 6.1 - Step 5 - 在浏览器中测试

[译] Laravel 5 之美 - 单元测试

运行 PHPUnit

Laravel 5.1 已经准备好测试了, 有个最简单的方式来检测一个访问是否返回200响应.

运行 PHPUnit , 简单的在根目录上执行 phpunit就OK

运行 PHPUnit

$ cd l5beauty
~/l5beauty $ phpunit
PHPUnit 4.7.4 by Sebastian Bergmann and contributors.

Time: 544 ms, Memory: 10.25Mb

OK (1 test, 2 assertions)

是否有错误 ?

如果你在运行 phpunit 收到 command not found 或者 permissions denied 错误提示, 有可能是因为安装问题. phpunit 命令一般会存放在 vendor/bin 目录并且添加进系统变量, 问题是 Laravel 命令有一个bug是没有给这个命令设置相应的权限

使用如下方法解决这个问题:

Step 1 - 删除 vendor 目录

Step 2 - 在代码根目录使用 composer update 命令重新创建 vendor 目录. (操作系统中运行)

这样, 然后重新执行 phpunit

Laravel 5.1 PHPUnit 配置

在 Laravel 5.1 项目根目录中有个文件 phpunit.xml. 这个文件包含使用 phpunit 运行时候的配置

phpunit.xml的测试会放置在 tests 目录, 这里有两个文件

  1. ExampleTest.php - 包含一个测试方法 testBasicExample(). 这个 ExampleTest 类集成自 TestCase 类.
  2. TestCase.php - Laravel 基础测试单元.

查看 testBasicExample() 方法 ExampleTest.php.

testBasicExample() 方法

public function testBasicExample()
  {
    $this->visit('/')
         ->see('Laravel 5');
  }

这个测试告诉我们 "访问主页并且能够看到内容 ‘Laravel 5’", 还能比这个更简洁么 ?

TestCase 类提供在框架中的应用方法和属性. TestCase 同样提供了一个附加断言列表方法和 crawler 类型测试

Laravel 5.1 Crawler 方法和属性

Crawler 允许你测试web应用. 这些方法都有个统一的优点就是都能够返回 $this , 允许你创建 ->visit()->see() 类似这样的链式调用.

下边是一些属性和方法:

$this->response

web应用返回的最后的响应

$this->currentUri

当前查看的Uri

visit($uri)

(Fluent) 使用 get 方法访问给定的uri

get($uri, array $headers = [])

(Fluent) 使用 get 方法访问url, 并可以传输给定的header

post($uri, array $data = [], array $headers = [])

(Fluent) post 请求

put($uri, array $data = [], array $headers = [])

(Fluent) put 请求

patch($uri, array $data = [], array $headers = [])

(Fluent) PATCH 请求

delete($uri, array $data = [], array $headers = [])

(Fluent) DELETE 请求

followRedirects()

(Fluent) 跟踪最近返回的重定向

see($text, $negate = false)

(Fluent) 查找页面上显示的内容/显示/不显示

seeJson(array $data = null)

(Fluent) 判定请求包含 json, 如果传输了 $data 参数, json 值必须匹配.

seeStatusCode($status)

(Fluent) 相应是否返回指定的状态码

seePageIs($uri)

(Fluent) 当前页面是否是指定的URI

seeOnPage($uri) and landOn($uri)

(Fluent) Alias seePageIs()

click($name)

(Fluent) 通过name 或者 id 来请求点击

type($text, $element)

(Fluent) 填充自定义的文本

check($element)

(Fluent) 检测页面的checkbox

select($option, $element)
(Fluent) 选择下拉项

attach($absolutePath, $element)

(Fluent) 附件

press($buttonText)

(Fluent) 提交指定文本的text

withoutMiddleware()

(Fluent) 禁用 middleware

dump()

输入最近返回的内容

Laravel 5.1 PHPUnit 应用方法

这里有额外的 Laravel 5.1 方法和属性

$app

$app 实例

$code

artisan 返回的最近的 code 码

refreshApplication()

刷新应用, setup()方法会自动调用这个方法.

call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)

调用指定的url 并且返回响应.

callSecure($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)

调用 https 访问url并且返回响应

action($method, $action, $wildcards = [], $parameters = [], $cookies = [], $files = [], $server = [], $content = null)

调用控制器方法

route($method, $name, $routeParameters = [], $parameters = [], $cookies = [], $files = [], $server = [], $content = null)

调用路由并且返回方法.

instance($abstract, $object)

注册对象的实例

expectsEvents($events)

指定可能被触发的事件列表.

withoutEvents()

不调用事件

expectsJobs($jobs)

注册队列

withSession(array $data)

使用session

session(array $data)

开始并设置 session

flushSession()

刷新当前session 的内容

startSession()

开始 session

actingAs($user)

(Fluent) 设置当前登录的用户

be($user)

设置用户

seeInDatabase($table, array $data, $connection = null)

(Fluent) 检测给定的数据是否存在在数据库中

notSeeInDatabase($table, $array $data, $connection = null)

(Fluent) 检测数据是否不存在数据库中

missingFromDatabase($table, array $data, $connection = null)

(Fluent) Alias notSeeInDatabase().

seed()

数据库数据seed 生成器

artisan($command, $parameters = [])

指定 artisan 命令并且返回代码

上边的方法/属性都能够在 test 中 使用, 默认的 ExampleTest.php 中存在一个方法 testBasicExample(), 这个调用了 $this->call(...) 方法.

Laravel 5.1 PHPUnit 断言

除了标准的 PHPUnit 断言(assertEquals(), assertContains(), assertInstanceOf(), ...)之外, 还存在很多允许测试 web 应用的检测项目

assertPageLoaded($uri, $message = null)

检测最近的页面是否被加载, 如果不存在 url / message 时候会报错

assertResponseOk()

是否页面相应OK

assertReponseStatus($code)

是否响应指定的code

assertViewHas($key, $value = null)

视图中是否存在指定的数据

assertViewHasAll($bindings)

视图中是否存在指定的一系列数据

assertViewMissing($key)

指定视图中是否不存在这个数据

assertRedirectedTo($uri, $with = [])

检测是否重定向到指定的uri

assertRedirectedToRoute($name, $parameters = [], $with = [])

是否客户端重定向到指定的路由

assertRedirectedToAction($name, $parameters = [], $with = [])

是否重定向到 action

assertSessionHas($key, $value = null)

session 中是否存在 key/ value

assertSessionHasAll($bindings)

session 中是否存在指定的 kv

assertSessionHasErrors($bindings = [])

session 是否存在错误

assertHasOldInput()

session 中是否存在以前的数据

使用 Gulp 来进行 TDD 测试

Gulp 是用javascript 写成的编译和自动化工具. 基本用来最小化源代码或者从源代码生成文件. Gulp 能够监控源代码的改变并且自动运行指定的任务

Laravel 5.1 存在 Laravel Elixir 允许运行 gulp 任务. Elixir 加入了更简洁的语法. 你这样想 PHP 中的 Laravel, Gulp 中的 Elixir.

一个最常用的 Gulp 是自动化测试. 我们根据 TDD (Test Driven Development) 来自动化运行我们的测试任务.

首先, 编辑 gulpfile.js 文件, 并按照以下修改.

配置 Gulp 来运行 PHPUnit 测试

var elixir = require('laravel-elixir');

elixir(function(mix) {
    mix.phpUnit();
});

这里我们调用 elixir() 方法. 传递一个函数. 这个函数接收一个 mix 对象. 这个函数能够干很多你可能想都想不到的事情. 你可能想通过 less 文件编译 css 文件. 然后合并 css 文件, 然后再文件末尾加缀上版本号. 所有的这些事情都可以通过 mix 对象来运行.

但是现在, 我们仅仅运行 PHPUnit 测试.

接下来, 直接运行 glup

运行 Gulp

~% cd Code/l5beauty
~/Code/l5beauty% gulp
[15:26:23] Using gulpfile ~/Code/l5beauty/gulpfile.js
[15:26:23] Starting 'default'...
[15:26:23] Starting 'phpunit'...
[15:26:25] Finished 'default' after 2.15 s
[15:26:25]

*** Debug Cmd: ./vendor/bin/phpunit --colors --debug ***

[15:26:28] PHPUnit 4.7.4 by Sebastian Bergmann and contributors.

Configuration read from /Users/chuck/Code/l5beauty/phpunit.xml

Starting test 'ExampleTest::testBasicExample'.

Time: 2.07 seconds, Memory: 10.25Mb

OK (1 test, 2 assertions)
[15:26:28] gulp-notify: [Green!]
[15:26:28] Finished 'phpunit' after 4.96 s

你可能收到一个通知, 一个弹框, 绿色告知你所有测试都已经通过了

Figure 6.2 - Gulp’s PHPUnit Success on Windows 8.1

[译] Laravel 5 之美 - 单元测试

想要使用 gulp进行自动化测试, 运行 gulp tdd

运行 Gulp

~% cd Code/l5beauty
~/Code/l5beauty% gulp tdd
[15:29:49] Using gulpfile ~/Code/l5beauty/gulpfile.js
[15:29:49] Starting 'tdd'...
[15:29:49] Finished 'tdd' after 21 ms

这个命令挂载在这里, 监听源文件的变化, 并且在需要的时候进行单元测试.

想要查看如何运行的. 让我们中段存在的单元测试.

改变 tests/ExampleTest.phpsee() 方法.

中段 ExampleTest.php

->see('Laravel 5x');

当你保存这个文件, gulp 将会通知并且重新运行 PHPUnit , 这个将会执行错误. 然后你会看到一个红色的错误提示

Figure 6.3 - Gulp’s PHPUnit Failure on Mac

[译] Laravel 5 之美 - 单元测试

如果你要重新更改回来,保存, 然后 gulp 会重新运行 PHPUnit, 然后你会看到绿色的图标

退出 Gulp’s tdd 模式

按下 Ctrl+C

创建 Markdown 服务

在博客应用中我们会使用 Markdown 语法来写文章, 如果你不熟悉 markdown, 可以通过连接来检查这个语法, 这是一个快速读/写并且能够保存为 HTML.

举例说明, 我们会创建一个服务项目来生成HTML.

拉取一个 Markdown 包

这里有许多的PHP包来把markdown 转换为HTML, 如果你去 http://packagist.org 这里搜索 markdown , 会发现 20 多页的包.

我们会使用 Michel Fortin 创建的包. 因为"好", 接下来我们运行如下的命令来拉取这个包

添加 Markdown 和 SmartyPants

~/Code/l5beauty% composer require michelf/php-markdown
Using version ^1.5 for michelf/php-markdown
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing michelf/php-markdown (1.5.0)
    Downloading: 100%

Writing lock file
Generating autoload files
Generating optimized class loader

~/Code/l5beauty% composer require "michelf/php-smartypants=1.6.0-beta1"
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing michelf/php-smartypants (1.6.0-beta1)
    Loading from cache

Writing lock file
Generating autoload files
Generating optimized class loader

你有没有注意到指定了包版本号. 这是因为写这篇文章的时候还没有一个稳定的版本号能够自动拉取到

创建 Markdown 测试类

开始 TDD session 的第一件事就是开启 TDD 模式

Starting Gulp in TDD mode

~/Code/l5beauty% gulp tdd
[19:41:38] Using gulpfile ~/Code/l5beauty/gulpfile.js
[19:41:38] Starting 'tdd'...
[19:41:38] Finished 'tdd' after 23 ms

现在 gulp 监控 PHPUnit 的改变. 让我们创建测试类

tests 目录, 创建一个新文件夹命名为 Services 同时创建一个文件 MarkdownerTest.php

初始化 tests/Services/MarkdownerTest.php

<?php

class MarkdownerTest extends TestCase
{

    # 保存 markdown 对象
    protected $markdown;
    
    # 创建 Markdowner 对象
    public function setup()
    {
        $this->markdown = new \App\Services\Markdowner();
    }
    
    # 测试
    public function testSimpleParagraph()
    {
        $this->assertEquals(
            "<p>test</p>\n",
            $this->markdown->toHTML('test')
        );
    }
}

这里会报错

即使告诉你检测失败, 日志中也会存在相应的错误日志, 这里很明显的是 App\Services\Markdowner 这个类不存在.

创建 Markdowner 服务

这里我们创建一个服务来封装 php-markdown 和 php-smartypants 来加载导入的服务

app\Services 目录创建一个 Markdowner.php 服务并填写以下的内容 .

app/Services/Markdowner 的内容

<?php
# 命名空间
namespace App\Services;
# 导入的类
use Michelf\MarkdownExtra;
use Michelf\SmartyPants;

class Markdowner
{
    
    # 转换类
    public function toHTML($text)
    {
        $text = $this->preTransformText($text);
        $text = MarkdownExtra::defaultTransform($text);
        $text = SmartyPants::defaultTransform($text);
        $text = $this->postTransformText($text);
        return $text;
    }
    
    protected function preTransformText($text)
    {
        return $text;
    }
    
    protected function postTransformText($text)
    {
        return $text;
    }
}

当你保存了这个文件, Gulp 应当显示一个绿色的提示框. 告诉你执行OK.

如果你没有收到绿色的提示. 检测文件和测试类.

更多测试

大家都同意的是, 这不是一个好的 TDD 测试例子, 因为太简单了. 实际的会有好多操作步骤和迭代, 如下

  • 创建 MarkdownerTest w/ testSimpleParagraph()
  • Tests Fail
  • 创建 Markdowner 类, 硬编码 toHTML() 来通过测试
  • Tests Succeed
  • 更新 Markdowner 类来使用 MarkdownExtra
  • Tests Succeed
  • 添加一个 testQuotes() 到 MarkdownerTest 类
  • Tests Fail
  • 更新 Markdowner 类来使用 SmartyPants
  • Tests Succeed

目前为止, 所有的我们 Markdowner 类在测试前都是不完整的.要在该类上进行 单元测试,应该将其结构化以便将 MarkdownExtraSmartyPants 类的实例注入到构造函数中, 通过这种方式我们的单元测试可以注入模拟对象,并且只验证 MarkdownExtra 的行为,而不是它所调用的类.

但这不是一本关于测试的书。事实上,这是讨论测试的惟一一章。我们会离开这个结构,但需要再添加几个测试。

更新 MarkdownerTest 来和下面的内容一致

app/Services/Markdowner 的最终内容

<?php

class MarkdownerTest extends TestCase
{

    protected $markdown;
    
    public function setup()
    {
        $this->markdown = new \App\Services\Markdowner();
    }
    
    /**
    * @dataProvider conversionsProvider
    */
    public function testConversions($value, $expected)
    {
        $this->assertEquals($expected, $this->markdown->toHTML($value));
    }
    
    public function conversionsProvider()
    {
        return [
            ["test", "<p>test</p>\n"],
            ["# title", "<h1>title</h1>\n"],
            ["Here's Johnny!", "<p>Here&#8217;s Johnny!</p>\n"],
        ];
    }
}

在这里,我们更改了测试类用来一次测试多个转换,并在 conversionsProvider() 中添加了三个测试。在进行下一步骤之前,你的测试结果应该是绿色的。

在系统控制台中一旦测试是绿色的,按 Ctrl+C 来停止 Gulp

测试的其他方法

这里并不是想使用 Laravel 5.1 提供一个完整的测试方法,因为在PHP中没有单独的方法来进行测试。

因此,在 Laravel 5 5.1 中没有单一的测试方法。

但是,我们将探索一些替代方案

phpspec

除了 PHPUnit ,Laravel 5.1 还提供了 phpspec 。这是另一种流行的PHP测试,它更多地关注于 BDD(行为驱动开发)

这里有一些关于phpspec的注释。

  • 程序文件在 vendor/bin, 因此你可以在项目根目录下调用 phpspec.
  • 配置文件在根目录, 名字是 phpspec.yml.
  • 从 Gulp 中运行 phpspec , Elixir 提供了 phpSpec() 函数, 你可以在 mix 对象中运行.
  • 如果你把程序的命名空间从 App 改成其他的命名空间, 确保同步更新 phpspec.yml 中的配置.

单元测试

虽然 PHPUnit 是 PHP 单元测试的标准,但也有其他的包可以使用

  • Enhance PHP - 单元测试框架支持 mocks 和 stubs.
  • SimpleTest - 另一个使用 mock 的单元测试框架.

功能 / 验收测试

这些测试真实的使用了您的应用程序,而不是仅仅验证您的应用程序中的代码单元。当使用流畅的测试方法 Laravel 5.1 时,你可以使用 PHPUnit 进行一些功能测试. ExampleTest.php 提供了一个简单的示例. 但是也有其他的测试框架关注于功能/验收测试(functional / acceptance testing).

行为驱动开发 (BDD)

BDD(行为驱动开发)有两种方式: SpecBDD 和 StoryBDD

SpecDD 关注代码的技术方面。Laravel 5.1 包含了 _phpspec_, 这是SpecDD的标准

StoryBDD 强调业务或特性测试. Behat 是最流行的 StoryBDD 框架。同样 Codeception 也可以用于 StoryBDD 。

回顾

我们在这一章所做的第一件事就是创建一个名为 l5beauty 的项目。然后我们在这个项目中使用 PHPUnit 进行单元测试。最后,我们创建了一个 Markdowner 服务类,这有两个目的 一是测试, 二是并在以后将 markdown 转换为 HTML 。

这是一个相当长的章节,因为测试是一个很大的话题,而一个章节无法给出公正的评价。但是,正如我所提到的,测试并不是本书的重点。在随后的章节中,将不再进行测试。

下一章我们讨论一些如何让系统更快的话题如何?

相关推荐