测试驱动开发(英语:Test-driven development,缩写为TDD)
是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名
- 单元测试是针对你代码中相对独立而且非常少的一部分代码来进行测试。实际上,大多数单元测试可能都是针对某一个方法来进行的。
- 功能测试是针对你代码中大部分的代码来进行测试,包括几个对象的相互作用,甚至是一个完整的 HTTP 请求 JSON 实例。
一.准备Laravel测试套件
在项目根目录中,更新phpunit.xml文件的如下项目:
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="API_DEBUG" value="false"/>
<ini name="memory_limit" value="512M" />
更新完后phpunit.xml文件会像下面这样:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="APP_DEBUG" value="false"/>
<env name="MAIL_DRIVER" value="log"/>
<ini name="memory_limit" value="512M" />
</php>
</phpunit>
我们只需要在内存中进行测试这样测试运行的速度会快一些,所以在database配置项目中我们将使用sqlite和:memory: (Sqlite的内存数据库)。 将APP_DEBUG设置为false因为我们只需要对真实产生的错误进行断言。随着项目迭代测试用例会越来越多所以在将来你可能会需要增加memory_limit的值。
译者注: APP_DEBUG建议不要改成false,这样有助于让我们的代码写的更严谨
在Laravel里测试用例的基类TestCase中作一些测试相关的准备:
<?php
namespace Tests;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Faker\Factory as Faker;
/**
* Class TestCase
* @package Tests
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
abstract class TestCase extends BaseTestCase
{
use CreatesApplication, DatabaseMigrations, DatabaseTransactions;
protected $faker;
/**
* Set up the test
*/
public function setUp()
{
parent::setUp();
$this->faker = Faker::create();
}
/**
* Reset the migrations
*/
public function tearDown()
{
$this->artisan('migrate:reset');
parent::tearDown();
}
}
我们需要在TestCase中use DatabaseMigrations这个trait这样在执行每个测试用例时,迁移文件都会被执行一遍,于此同时在setUp()和tearDown()方法中我们需要执行创建测试环境和清理测试环境的相关操作。
译者注: DatabaseMigrations这个性状在测试setUp阶段会执行migrate:refresh, 清除所有表然后重新执行迁移, 其实我们重置数据库后需要通过seeder来填充测试数据,所以我更倾向于不使用这个迁移而是在TestCase的setUp中 使用migrate:refresh —seeder 来完成重置数据库和填充测试数据的操作
二.编写测试用例
为了让phpunit识别测试,需要在测试方法上添加/ @test /注释,或者是测试方法命名以test前缀开头
<?php
namespace Tests\Unit;
use Tests\TestCase;
class ArticleApiUnitTest extends TestCase
{
/**
* @test
*/
public function it_can_create_an_article()
{
$data = [
'title' => $this->faker->sentence,
'content' => $this->faker->paragraph
];
$this->post(route('articles.store'), $data)
->assertStatus(201)// 断言成功后返回201状态码
->assertJson($data);// 断言json数据
}
}
在这个测试中,测试了是否能创建一篇文章,我们断言了在创建文章成功后应用将返回201状态码还有预期的JSON数据。
在创建好我们的第一个测试后,执行phpunit或者vendor/bin/phpunit
当我们执行phpunit后测试结果显示失败了,
这很正常因为在测试驱动开发中我们是先写测试程序,然后在编码实现功能的,
所以在创建测试程序伊始测试程序执行后的结果就是测试失败(测试驱动开发的第二条原则)。
在测试中我们断言应用会返回201状态码但是却返回了404,为什么?
因为测试中请求的URL还未在应用中创建。
三.在路由文件中创建测试里请求的URL
Route::resource('articles', ArticlesApiController::class);
你可以通过artisan命令创建这个资源控制器
资源控制器遵循restfulApi.
php artisan make:controller ArticlesApiController —-resource
也可以手动创建。
POST请求将会路由到ArticlesApiContorller的store方法
四.DEBUG控制器
<?php
namespace App\Http\Controllers\Api;
class ArticlesApiController extends Controller
{
public function store() {
dd('success!');
}
}
再次执行phpunit
五.验证你的输入
不要忘记验证将要存储到数据库中的数据,
所以现在我们创建一个CreateArticleRequest类来控制输入数据的验证。
<?php
namespace App\Http\Controllers\Api;
class ArticlesApiController extends Controller
{
public function store(CreateArticleRequest $request) {
dd('success!');
}
}
这个请求类包含了数据验证的规则:
<?php
namespace App\Articles\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateArticleRequest extends FormRequest
{
/**
* Transform the error messages into JSON
*
* @param array $errors
* @return \Illuminate\Http\JsonResponse
*/
public function response(array $errors)
{
return response()->json($errors, 422);
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'title' => ['required'],
'content' => ['required']
];
}
}
这是一个很好的验证数据的方式,因为之后我们可以新建另外一个测试程序来测试是否能捕获到这些验证错误,不过为了文章的精简我会在另外一篇单独的文章里来讲述如何创建捕获错误的测试程序。
六.返回刚才创建的数据
记住在相应中应该返回指定的JSON结构这样我们就知道新建的数据成功存储到了数据库中。所以我们应该返回新创建的文章对象来满足我们上面创建的测试程序。
<?php
namespace App\Http\Controllers\Api;
class ArticlesApiController extends Controller
{
/**
* @param CreateArticleRequest $request
*/
public function store(CreateArticleRequest $request) {
return Article::create($request->all());
}
}
你可能已经已经注意到了我们还没有创建Article这个类,接下来让我们来创建这个Model。
七.创建模型类
<?php
namespace App\Articles;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $table = 'articles';
protected $fillable = [
'title',
'content'
];
}
在Article这个模型类中,你需要定义可填充和序列化时需要被隐藏的字段,一旦Article类定义好后,返回到控制器中导入这个Article类。
<?php
namespace App\Http\Controllers\Api;
use App\Articles\Article;
class ArticlesApiController extends Controller
{
/**
* @param CreateArticleRequest $request
*/
public function store(CreateArticleRequest $request) {
return Article::create($request->all());
}
}
我们差不多快要完成了!测试程序正确建立好了,URL已经创建并且能够访问了,处理URL请求的控制器程序还有与数据表对应的模型类也都已经就绪了,现在让我们再试着执行一次phpunit命令。
八.再次执行phpunit看看结果会怎么样
它再一次失败了,这是好事还是坏事?可以说即是好事也是坏事。好的一方面是测试中断言会返回201状态码的URL的返回结果从之前的404错误变成了500错误(如果你注意到了)。
不好的一方面是,它测试失败了我们需要让程序能够正确通过测试程序。当我们想要debug时我们想要看看应用程序到底抛出了什么错误。在Laravel测试程序中你只需要在发出POST请求后在调用dump()方法就能够看到应用程序返回的响应。
<?php
namespace Tests\Unit;
use Tests\TestCase;
class ArticleApiUnitTest extends TestCase
{
public function it_can_create_an_article()
{
$data = [
'title' => $this->faker->sentence,
'content' => $this->faker->paragraph
];
$this->post(route('articles.store'), $data)
->dump()
->assertStatus(201)
->assertJson($data);
}
}
你可以进一步debug POST请求后的输出,没准会得到更多你需要的信息,如果没有明确地给出提示发生了什么才导致的这个错误你可以去Laravel应用的日志文件/storage/logs/laravel.log里去查找错误信息。
现在让我们检查一下为什么会返回500错误。
Well, 因为我们在请求中正在尝试向一个不存在的数据表中写入数据所以才请求才会返回500错误。
九.创建数据表
执行下面的laravel artisan命令:
php artisan make:migration create_articles_table –create=articles
Laravel会自动在/database/migrations里来创建迁移文件。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateArticlesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('articles', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('articles');
}
}
默认迁移创建的数据表中只有id和时间字段,你需要根据需求添加需要的字段。
十.再一次运行phpunit
修改控制器
public function store(CreateArticleRequest $request)
{
$article = Article::create($request->all());
return response()->json($article, 201);
}
十一.再次执行phpunit
祈祷成功