Laravel 项目:使用 TDD 构建论坛 Chapter 11
0.写在前面
- 本系列文章为
laracasts.com
的系列视频教程——Let's Build A Forum with Laravel and TDD 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频, 支持正版 。 - 视频源码地址:https://github.com/laracasts/Lets-Build-a-Forum-in-Laravel
- 本项目为一个 forum(论坛)项目,与本站的第二本实战教程 Laravel 教程 - Web 开发实战进阶 ( Laravel 5.5 ) 类似,可互相参照
- 项目开发模式为
TDD
开发,教程简介为:A forum is a deceptively complex thing. Sure, it's made up of threads and replies, but what else might exist as part of a forum? What about profiles, or thread subscriptions, or filtering, or real-time notifications? As it turns out, a forum is the perfect project to stretch your programming muscles. In this series, we'll work together to build one with tests from A to Z
- 项目版本为
laravel 5.4
,教程后面会进行升级到laravel 5.5
的教学 - 视频教程共计 102 个小节,笔记章节与视频教程一一对应
1.本节说明
- 对应视频第 11 小节:A User Can Filter Threads By Channel
2.本节内容
我们引入了Channel
的概念:一个Thread
属于一个Channel
,一个Channel
拥有多个Thread
。现在我们来实现根据Channel
筛选Thread
的功能。
首先新建测试:forum\tests\Feature\ReadThreadsTest.php
.
.
/** @test */
public function a_user_can_filter_threads_according_to_a_channel()
{
$channel = create('App\Channel');
$threadInChannel = create('App\Thread',['channel_id' => $channel->id]);
$threadNotInChanne = create('App\Thread');
$this->get('/threads/' . $channel->slug)
->assertSee($threadInChannel->title)
->assertDontSee($threadNotInChanne->title);
}
.
.
在测试用例中,我们新建了一个Channel
两个Thread
,其中一个Thread
的channel_id
是我们新建Channel
的id
。我们的测试是,当我们通过该Channle
来筛选Thread
,我们希望看到与该Channel
相关的Thread
,并且不看到与该Channel
无关的Thread
。
运行测试:
让我们来修复它:forum\routes\web.php
.
.
Route::get('/home', 'HomeController@index')->name('home');
Route::get('threads/{channel}','ThreadsController@index'); -->修改此处路由
Route::get('threads/create','ThreadsController@create');
Route::get('threads/{channel}/{thread}','ThreadsController@show');
Route::post('threads','ThreadsController@store');
Route::post('/threads/{channel}/{thread}/replies','RepliesController@store');
.
.
forum\app\Http\Controllers\ThreadsController.php
.
.
public function index($channelSlug = null)
{
if($channelSlug){
$channelId = Channel::where('slug',$channelSlug)->first()->id;
$threads = Thread::where('channel_id',$channelId)->latest()->get();
}else{
$threads = Thread::latest()->get();
}
return view('threads.index',compact('threads'));
}
.
.
可以看到,我们在控制器中的代码是很粗糙的。我们这么做的原因是为了先让测试通过,稍后进行修改。现在来运行测试:
$ APP_ENV=testing phpunit --filter a_user_can_filter_threads_according_to_a_channel
测试通过:
现在我们的测试已经通过,接下来我们来完善刚刚编写的代码:
public function index(Channel $channel)
{
if($channel->exists){
$threads = $channel->threads()->latest()->get();
}else{
$threads = Thread::latest()->get();
}
return view('threads.index',compact('threads'));
}
注意到我们使用了
threads()
来获取$threads
,但模型关联尚未建立,需要先编写单元测试,这也是 TDD 的开发理念。
我们来编写单元测试:forum\tests\Unit\ChannelTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class ChannelTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_channel_consists_of_threads()
{
$channel = create('App\Channel');
$thread = create('App\Thread',['channel_id' => $channel->id]);
$this->assertTrue($channel->threads->contains($thread));
}
}
运行一下测试:
因为此时尚未建立模型关联,因此$thread->threads
获取的对象为null
,所以就抛出了上面的异常。我们来建立模型关联:forum\app\Channel.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Channel extends Model
{
public function threads()
{
return $this->hasMany(Thread::class);
}
}
运行测试:
测试已经 OK ,但是我们现在还有一个问题要注意:如果我们访问 http://forum.test/threads/{channel} 这样的路由,我们是无法访问的:
注意看一下我们的路由定义:
Route::get('threads/{channel}','ThreadsController@index');
我们知道,以上的路由符合 Laravel 的 隐性路由模型绑定 原则。但是{channel}
路由片段默认对应的是id
字段,而我们需要对应的是slug
字段。所以我们需要重写getRouteKeyName()
方法:forum\app\Channel.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Channel extends Model
{
public function getRouteKeyName()
{
return 'slug';
}
.
.
}
运行一下全部的测试:
$ APP_ENV=testing phpunit
先看一下失败的guests_may_not_create_threads
测试:forum\tests\Feature\CreateThreadsTest.php
.
.
/** @test */
public function guests_may_not_create_threads()
{
$this->withExceptionHandling();
$this->get('/threads/create')
->assertRedirect('/login');
$this->post('/threads')
->assertRedirect('/login');
}
.
.
注意看这一行$this->get('/threads/create')
。我们声明过/threads/{channel}
这样的路由,用来筛选该channel
下的所有相关thread
,在/threads/create
这样的url
中,Laravel 会认为create
是一个channel
。我们需要调整路由声明的顺序:forum\routes\web.php
.
.
Route::get('/home', 'HomeController@index')->name('home');
Route::get('threads','ThreadsController@index'); -->在之前的课程中误删除了此路由,因此测试时报了一个错误
Route::get('threads/create','ThreadsController@create');
Route::get('threads/{channel}/{thread}','ThreadsController@show');
Route::post('threads','ThreadsController@store');
Route::get('threads/{channel}','ThreadsController@index'); -->将此路由放在后面声明
Route::post('/threads/{channel}/{thread}/replies','RepliesController@store');
.
再次运行测试,即可成功通过:
现在我们已经可以根据channel
来筛选thread
了,让我们在页面展示出来:forum\resources\views\layouts\app.blade.php
.
.
<div class="collapse navbar-collapse" id="app-navbar-collapse">
<!-- Left Side Of Navbar -->
<ul class="nav navbar-nav">
<li><a href="/threads">All Threads</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-hidden="true"
aria-expanded="false">Channels <span class="caret"></span> </a>
<ul class="dropdown-menu">
@foreach(\App\Channel::all() as $channel)
<li><a href="/threads/{{ $channel->slug }}">{{ $channel->name }}</a> </li>
@endforeach
</ul>
</li>
</ul>
.
.
注:获取所有
channel
的代码稍后需要完善,目前的比较粗糙
看一下效果:
3.写在后面
- 如有建议或意见,欢迎指出~
- 如果觉得文章写的不错,请点赞鼓励下哈,你的鼓励将是我的动力!
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: