[新手开发记录] 从测试开始开发

我准备开发一个名校公开课的汉化资料下载和讨论版,功能就是站长发布信息和资料下载链接,用户可以在下面讨论,并且可以生成各种平台的分享方式分享出去。这一系列文章就记录这一过程。当然博文功能也是必不可少的。

测试驱动开发

测试是一个很重要的内容,我也不是很熟悉,只是跟着教程一步步去做,但是开发的东西不一样,我想开发我的东西,教程是教一个简单的网站和复杂的论坛,分别是 Laracasts 的 《Build A Laravel App With TDD》和 《Let’s Build A Forum with Laravel and TDD》,这两个系列都是在 Laravel From Scratch 之后可以进一步学习以深化技术的质量和深度都不错的课程,我就择取重点参考这几个课程来协助我完成这个网站的构建。
(中文语音版看这里:[完结] Laravel 6 From Scratch [Laracasts 免费视频中文语音]

注:TDD 是测试驱动开发(Test-Driven Development)。测试也有很多细分的东西,可以参考这里:laravel-china.github.io/php-the-rig...

新建测试类

我们在建立好网站的大体样式以后,马上就新建一个测试:

php artisan make:test CoursesTest

你可以在 test/Feature/ 目录下找到。
然后我们就写我们第一个测试,一个用户可以新建 course,逻辑是用户向服务器的一个 url 提交一个 post 请求,然后就可以在数据通过验证之后在数据库创建一个 course。

这里的 course 是指某一个系列课程的名称,它还可以属于某个分类,比如名校公开课,国外视频教程等等。然后每个 course 又由很多单个的课构成,一般称之为 episode 。除了构建关系部分,curd 部分实际上是差不多的,我们这里就以 course 举例。

整个文件代码此时应该看起来如下:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class CoursesTest extends TestCase
{
    use WithFaker, RefreshDatabase;

    /** @test */
    public function a_user_can_create_a_course()
    {
        $attributes = [
            'title' => $this->faker->sentence,
            'description' => $this->faker->paragraph,
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', $attributes);
    }
}

我们可以看到这类测试类扩展了 TestCase 类:

class CoursesTest extends TestCase

那么它继承了许多有用的方法,不了解的可以仔细看本站的这篇文章参考:Laravel 测试之 —— PHPUnit 入门教程

注:测试函数上面的 /** @test */ 记得加上,否则测试不识别。

解释

我们一段段来看。

第一段:

use WithFaker, RefreshDatabase;

这里使用了两个 trait,WithFaker 令我们可以在测试代码中使用 $this->faker 来方便的生成各种虚拟数据,十分方便,例如上述代码中的 $this->faker->sentence$this->faker->paragraphRefreshDatabase 可以让我们在运行完测试之后,数据库恢复测试前的状态。

第二段:

$this->post('/courses', $attributes);

这里通过测试实例提供的 post() 方法,模拟向应用发送了一个 POST 请求,把 $attributes 传入,然后服务器就对这些数据进行处理。

第三段:

$this->assertDatabaseHas('courses',  $attributes);

这是最终测试的检测过程,检测数据库表 courses 中是否能够获取这组数据 $attributes

运行测试

写完测试,可以通过终端运行:

vendor\bin\phpunit tests/Feature/CoursesTest.php

关于如何简化命令,要么设置一个快捷命令(参考本站帖子:让你懒到逆天的 Bash 别名),或者很多流行的编辑器都带有插件功能,可以利用插件快捷键快速运行测试。

那么这时候肯定会报错的,因为这时候除了测试什么都没有,这也是 laracasts 作者推荐的实际开发流程,也是测试驱动开发的含义,先写测试,然后根据错误一步步的完善程序功能。

存在的几个问题

这里显然存在几个问题:
1.没有数据库
我们需要新建数据库,并且配置数据库,配置如下:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

然后我们就需要根据配置文件中的 DB_DATABASE 的值来新建一个数据库,我这里演示一下 laragon 的方法,它自带一个数据库管理工具:

[新手开发记录] 从测试开始开发

点击 Database 按钮之后,会出来如下界面:

[新手开发记录] 从测试开始开发

可以看到右侧的用户名和密码还有端口号,和我们的配置一致,这是默认设置。
然后我们双击左侧 Laragon ,出现如下界面:

[新手开发记录] 从测试开始开发
我们在 Laragon 处邮件单击 Laragon 新建数据库:

[新手开发记录] 从测试开始开发
填入数据库名称,这里我填了 laravel,选择了字符编码,点击确定(OK)就完成了创建了。

[新手开发记录] 从测试开始开发

2.数据库没有 courses 表并且测试运行在我们应用程序的同一个数据库中
我们就需要做两件事:
第一,配置测试用的数据库
我们打开 ./phpunit.xml 进行一些配置,我截取一些配置代码,下面是原始的配置:

    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>

<php></php> 之间,我们添加一些配置行,name.env 文件中的配置项相同,这里配置了之后会覆盖 .env 中的配置,下面就是我们要添加的配置行:

        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>

第一行是覆盖了要连接的数据库类型,测试时,我们使用 sqlite,然后第二行表示数据库使用内存。
这时候我们再次运行测试,仍然会报一样的错误,但是这时候就不再和我们的应用共用数据库了。

有时候会报错:could not find driver,那是因为没有开启 PHP 的 sqlite 扩展,我在 php.ini 中取消了这两个配置的注释:extension=pdo_sqliteextension=sqlite3
如果你使用的是 laragon 可以直接在软件界面中找到 php.ini

[新手开发记录] 从测试开始开发

第二,生成我们应用的数据库表
上面说了,测试还是会报错,因为我们没有建立数据库表,我们需要生成数据库表格,我们运行下列命令来建立数据库迁移文件:

php artisan make:migration create_courses_table

这里提示一下,上面说的 RefreshDatabase 这个 trait ,它不仅会在测试结束后恢复数据库状态,还会在测试前进行必要的数据库迁移操作。
所以,如果我们再次运行测试,错误会再次更新,因为数据库表有了,我们虽然没有迁移它,但是测试时候 RefreshDatabase trait 会帮我们迁移,这时候它会寻找上面 $attributes 对应的数据库字段,但是没找到。

但是有个问题,大家有没有注意到,我们测试一直是运行到最后检测数据的时候报错,但是中间的提交数据的代码却从来没有报错,就是下面这一段代码:

$this->post('/courses', $attributes);

而实际上我们根本没有建立任何的路由,因此这里实际上也是存在错误或者异常的。
为什么会这样呢,因为 laravel 会默认帮我们处理异常,但是测试过程中这会让我们无法进行有效的检测,所以我们必须告诉 laravel,不必在意任何错误或者异常,都抛出来。
所以我们要在上述的测试类中再加一行代码,变成如下:

public function a_user_can_create_a_course()
    {
        $this->withoutExceptionHandling();

        attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', attributes);
    }

添加的代码行是:

        $this->withoutExceptionHandling();

就是阻止了 laravel 默认开启的异常处理。
这时候我们再测试,就会发现新的错误,在没有运行到最后判断数据的代码行时,已经抛出异常,没找到对应的请求路径。
所以我们要在 ./routes/web.php 中添加一个新的路由,如下:

Route::post('/courses', function(){
    //待写
});

然后再运行测试,错误又回到了刚才的,找不到 $attributes 对应的数据库字段。
所以在上面的路由闭包中,我们就需要把 $attributes 数据储存到数据库表中。
这就是很传统的三部曲:

Route::post('/courses', function(){
    //1.验证数据
    //2.储存数据
    //3.跳转网页
});

验证的事情我们以后会讲,这里直接讲储存数据,我直接把我希望的方式写出来:

Route::post('/courses', function(){
    //1.验证数据
    //2.储存数据
    App\Course::create(request(['title', 'description']));
    //3.跳转网页
});

当然又会报错,因为我们没有 App\Course 这个类。
于是运行如下:

php artisan make:model Course

然后继续测试,下一个错误是新手经常碰到的 mess assignment 异常,我们通过批量赋值的操作进行数据库的新建或者更新,laravel 默认会阻止这一行为。

这个保护机制通常是有用的,但是 laracasts 的作者多次表示它比较烦人,所以经常主动关闭它,不过大家模仿这个操作的时候务必弄清楚这个异常的本质,然后清楚自己在干什么。

关闭方法就是在 Course 的类文件中添加如下代码:

class Course extends Model
{
    protected $guarded = [];
}

然后再次测试,错误更新了,就是插入数据库时找不到 title 和 description 字段。
因为我们还没有修改我们的数据库迁移文件,生成数据库的时候没有生成我们需要的 title 和 description 字段,我们加上,使得数据库迁移文件中的 up() 函数如下:

    public function up()
    {
        Schema::create('courses', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description');
            $table->timestamps();
        });
    }

最后再运行一次测试,我们测试成功了。
那么这个测试的过程,我们再看一下:

    public function a_user_can_create_a_course()
    {
        $this->withoutExceptionHandling();

        attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', attributes);
    }

首先是通过一个 POST 请求,提交了一组数据,然后我们期望在数据库的 courses 表中看到这组数据。
但是这还不够,我们应当最后希望能够在视图中看到这些数据,比如在一个课程列表中。
所以我们继续修改我们的测试文件,在最后添加一行,我们希望在访问课程列表时,看到我们添加的课程的 title:

    public function a_user_can_create_a_course()
    {
        $this->withoutExceptionHandling();

        attributes = [
            'title' => $this->faker->sentence;
            'description' => $this->faker->paragraph;
        ];

        $this->post('/courses', $attributes);

        $this->assertDatabaseHas('courses', attributes);

        $this->get('/courses')->assertSee($attributes['title]);
    }

当然,运行的时候肯定也会报错,因为程序找不到对应的 url,我们还没有定义相应的路由。

注:对于已经写的两个路由不熟悉的话,可以参考国外教程中文视频:
1.七个 RESTful 控制器方法
2.RESTful路由
目前的情况,我们还没有专门的 Course 控制器,都是通过路由闭包实现处理,后面我们会重构。

我们修改路由文件:

Route::get('/courses', function(){
    $courses = App\Course::all();

    return view('courses.index', compact('courses));
});

运行测试,视图不存在。
我们就新建一个视图 /resources/views/courses/index.blade.php

<!DOCTYPE html>
<html>
<head>
    <title>Courses</title>
</head>
<body>

    <h1>Courses</h1>

    <ul>
        @foreach($courses as $course)
        <li>{{ $course->title }}</li>
        @endforeach
    </ul>

</body>
</html>

运行测试,通过!
本文就讲到这里,初步感受一下 TDD 的流程。

最后

测试驱动开发就是一个个的利用测试类提供的功能,测试先行,可用的方法很多,我就列举了一些,全部细节也不一一列出了。
有兴趣的可以关注公众号 laravelgo,然后根据提示加群,后续如果大家有兴趣,我可以把上面说的两个 TDD 开发系列精华进行整理汉化,汉化内容质量参考我B站更新的内容,精华整理分享应该比同声传译质量更高:B站地址,点击访问

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1

好棒好棒,受益匪浅

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!