2.测试话题
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 ;- 视频源码地址:github.com/laracasts/Lets-Build-a-...;
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 《Laravel 教程 - Web 开发实战进阶》 类似,可互相参照。
本节说明
- 对应视频第 2 小节:Testing Drving Threads
本节内容
第一个功能测试
以下是 Laravel 5.5 中文文档 中对 Laravel 测试 的简介,详细内容参见文档:
在运行测试时,Laravel 会根据 phpunit.xml 文件中设定好的环境变量自动将环境变量设置为 testing,并将 Session 及缓存以 array 的形式存储,也就是说在测试时不会持久化任何 Session 或缓存数据。
你可以随意创建其它必要的测试环境配置。testing 环境的变量可以在 phpunit.xml 文件中被修改,但是在运行测试之前,请确保使用 config:clear Artisan 命令来清除配置信息的缓存。
我们将Thread
定义为话题,现在我们来建立第一个简单的功能测试:a_user_can_browse_theads
。
首先重命名tests\Feature\ExampleTest.php
为ThreadsTest.php
,修改phpunit.xml
文件,配置测试环境:
.
.
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="MAIL_DRIVER" value="array"/>
</php>
.
.
我们在内存中进行测试,这样测试运行的速度会快一些,所以在 database 配置项目中我们将使用 sqlite 和 :memory: (Sqlite的内存数据库)。 随着项目迭代测试用例会越来越多所以在将来你可能会需要增加 memory_limit 的值。
编写测试方法:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class ThreadsTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_user_can_browse_threads()
{
$response = $this->get('/threads');
$response->assertStatus(200);
}
}
现在我们已经开始了 TDD 的第一次实践,在使用 TDD 进行开发时,请牢记以下三项法则:
- 在编写失败的测试之前,不要编写任何业务代码;
- 只要有一个单元测试失败了,就不要再写测试代码。无法通过编译也是一种失败情况;
- 业务代码恰好能够让当前失败的测试成功通过即可,不要多写;
接下来我们找到Illuminate\Foundation\Testing\DatabaseMigrations.php
文件:
<?php
namespace Illuminate\Foundation\Testing;
use Illuminate\Contracts\Console\Kernel;
trait DatabaseMigrations
{
/**
* Define hooks to migrate the database before and after each test.
*
* @return void
*/
public function runDatabaseMigrations()
{
$this->artisan('migrate');
$this->app[Kernel::class]->setArtisan(null);
$this->beforeApplicationDestroyed(function () {
$this->artisan('migrate:rollback');
});
}
}
可以看到,每次在进行测试的时候,都会先执行php artisan migrate
命令初始化数据库;每次执行完测试,都会执行php artisan migrate:rollback
命令重置数据库。
现在执行命令运行测试:
$ phpunit
会发现有报错,这是必然的,因为目前我们还未设置路由。前往web.php
文件添加路由配置:
Route::get('/threads','ThreadsController@index');
前往ThreadsController.php
添加index
方法:
public function index()
{
$threads = Thread::latest()->get();
return view('threads.index',compact('threads'));
}
注:
latest()
和oldest()
方法允许你轻松地按日期对查询结果排序。默认情况下是对 created_at 字段进行排序。或者,你可以传递你想要排序的字段名称:$user = DB::table('users') ->latest() ->first();
新建视图文件resources/views/threads/index.blade.php
:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">forum Threads</div>
<div class="panel-body">
@foreach($threads as $thread)
<article>
<a href="/threads/{{ $thread->id }}">
<h4>{{ $thread->title }}</h4>
</a>
<div class="body">{{ $thread->body }}</div>
</article>
<hr>
@endforeach
</div>
</div>
</div>
</div>
</div>
@endsection
顺手生成一下 Laravel 自带的注册登录功能:
$ php artisan make:auth
再次执行命令即可成功运行:
这意味着访问 forum.test/threads ,可以看到:
但是,此时的功能测试仅仅代表可以访问该路由,并未达到功能测试的要求。接下来编写真正的测试逻辑:
public function a_user_can_browse_threads()
{
$thread = factory('App\Thread')->create();
$response = $this->get('/threads');
$response->assertSee($thread->title);
}
运行测试:
$ phpunit
测试通过:
此时如果我们去掉index.blade.php
视图中title
属性,则应该测试失败:
.
.
<div class="panel-body">
@foreach($threads as $thread)
<article>
//
</article>
<hr>
@endforeach
.
.
运行测试,发现测试失败:
这证明我们的测试有效。我们继续编写测试,测试单个thread
:
public function a_user_can_browse_threads()
{
$thread = factory('App\Thread')->create();
$response = $this->get('/threads');
$response->assertSee($thread->title);
$response = $this->get('/threads/' . $thread->id);
$response->assertSee($thread->title);
}
运行测试依然会失败,因为还未添加路由、控制器方法跟视图。修改web.php
:
.
.
Route::get('/threads','ThreadsController@index');
Route::get('/threads/{thread}','ThreadsController@show');
修改ThreadsController.php
:
.
.
public function show(Thread $thread)
{
return view('threads.show',compact('thread'));
}
.
.
新建resources/views/threads/show.blade.php
:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
{{ $thread->title }}
</div>
<div class="panel-body">
{{ $thread->body }}
</div>
</div>
</div>
</div>
</div>
@endsection
再次测试,测试成功:
此时访问 forum.test/threads/1 :
此时我们的测试是放在一个方法中的,不但可读性不高,并且在功能测试不通过时难以准确定位。让我们将单个测试拆分成a_user_can_view_all_threads
和a_user_can_read_single_thread
两个测试:
use DatabaseMigrations;
/** @test */
public function a_user_can_view_all_threads()
{
$thread = factory('App\Thread')->create();
$response = $this->get('/threads');
$response->assertSee($thread->title);
}
/** @test */
public function a_user_can_read_a_single_thread()
{
$thread = factory('App\Thread')->create();
$response = $this->get('/threads/' . $thread->id);
$response->assertSee($thread->title);
}
运行测试,成功通过测试:
此时在我们的视图文件中,我们采用的是使用url
的方式给文章标题附上超链接。可是这种方法可读性差且不利于维护,现在进行修改。
首先在app\Thread.php
模型中新增path
方法,用来获取链接:
.
.
public function path()
{
return '/threads/'.$this->id;
}
.
接着修改视图文件index.blade.php
:
.
.
<article>
<a href="{{ $thread->path() }}">
<h4>{{ $thread->title }}</h4>
</a>
<div class="body">{{ $thread->body }}</div>
</article>
.
.