Laravel 测试驱动开发 -- 反向单元测试

Negative CRUD Unit Testing in Laravel5

这是一篇译文,原文链接: https://medium.com/@jsdecena/negative-crud...

作为CRUD Unit Testing in Laravel5的第二部分,在这篇文章中我们将来讨论反向测试。

上一篇我们写的都是正向测试;断言可以createupdateshow或者delete Carousel模型对象,现在让我们进行方向测试,看看如果在执行上面那些动作失败的情况下我们应该如何控制他们?

从create测试开始

<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
    /** @test */
    public function it_should_throw_an_error_when_the_required_columns_are_not_filled()
    {
        $this->expectException(CreateCarouselErrorException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->createCarousel([]);
    }
}

还记得吗在创建carousel的migration文件时,我们把link字段设置为可空,而titlesrc字段设置成了不允许为空。

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCarouselTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('carousels', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('link')->nullable();
            $table->string('src');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('carousels');
    }
}

所以我们预期当尝试设置titlesrc为NULL的时候数据库应该抛出一个错误,对吧?好消息是我们在respository类中捕获到了这个错误。

<?php
namespace App\Shop\Carousels\Repositories;
use App\Shop\Carousels\Carousel;
use App\Shop\Carousels\Exceptions\CreateCarouselErrorException;
use Illuminate\Database\QueryException;
class CarouselRepository
{
    /**
     * CarouselRepository constructor.
     * @param Carousel $carousel
     */
    public function __construct(Carousel $carousel)
    {
        $this->model = $carousel;
    }
    /**
     * @param array $data
     * @return Carousel
     * @throws CreateCarouselErrorException
     */
    public function createCarousel(array $data) : Carousel
    {
        try {
            return $this->model->create($data);
        } catch (QueryException $e) {
            throw new CreateCarouselErrorException($e);
        }
    }
}

在Laravel中数据库错误会抛出QueryException异常,所以我们捕获了这个异常然后创建了一个可读性更高的异常CreateCarouselErrorException

<?php
namespace App\Shop\Carousels\Exceptions;
class CreateCarouselErrorException extends \Exception
{
}

这些准备好后,运行phpunit然后看看会发生什么。

PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 993 ms, Memory: 26.00MB
OK (1 test, 1 assertion)

上面的结果意味着我们正确地捕获到了这个异常。

之后我们可以在控制器里捕获这个异常并在异常发生时定义我们自己需要的行为。

read test

如果查找不到Carsouel模型对象应该怎么办?

<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
    /** @test */
    public function it_should_throw_not_found_error_exception_when_the_carousel_is_not_found()
    {
        $this->expectException(CarouselNotFoundException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->findCarousel(999);
    }
    /** @test */
    public function it_should_throw_an_error_when_the_required_columns_are_not_filled()
    {
        $this->expectException(CreateCarouselErrorException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->createCarousel([]);
    }
}

回到repository类中看看我们的findCarousel()方法

<?php
namespace App\Shop\Carousels\Repositories;
use App\Shop\Carousels\Carousel;
use App\Shop\Carousels\Exceptions\CarouselNotFoundException;
use App\Shop\Carousels\Exceptions\CreateCarouselErrorException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
class CarouselRepository
{
   protected $model;

    /**
     * CarouselRepository constructor.
     * @param Carousel $carousel
     */
    public function __construct(Carousel $carousel)
    {
        $this->model = $carousel;
    }
    ...

    /**
     * @param int $id
     * @return Carousel
     * @throws CarouselNotFoundException
     */
    public function findCarousel(int $id) : Carousel
    {
        try {
            return $this->model->findOrFail($id);
        } catch (ModelNotFoundException $e) {
            throw new CarouselNotFoundException($e);
        }
    }

     ...
}

findCarousel()方法中我们捕获了Laravel的findOrFail()在找不到模型时默认抛出的ModelNotFoundException

现在再次运行phpunit

PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 936 ms, Memory: 26.00MB
OK (1 test, 1 assertion)

看起来不错,那么如果无法update时我们该怎么办?

update test

<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
    /** @test */
    public function it_should_throw_update_error_exception_when_the_carousel_has_failed_to_update()
    {
        $this->expectException(UpdateCarouselErrorException::class);
        $carousel = factory(Carousel::class)->create();
        $carouselRepo = new CarouselRepository($carousel);
        $data = ['title' => null];
        $carouselRepo->updateCarousel($data);
    }  

    /** @test */
    public function it_should_throw_not_found_error_exception_when_the_carousel_is_not_found()
    {
        $this->expectException(CarouselNotFoundException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->findCarousel(999);
    }
    /** @test */
    public function it_should_throw_an_error_when_the_required_columns_are_not_filled()
    {
        $this->expectException(CreateCarouselErrorException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->createCarousel([]);
    }

你可以看到,在上面的测试程序里我们有意地将title字段设置成了null,因为在上一个测试中把title设为null在创建Carousel时就会抛出错误。所以我们假设数据库的记录中title已经有值了。

Note: 当在测试程序中断言异常时,应该把断言异常的语句放在测试方法的顶部

来看一下repository里的updateCarousel()方法

<?php
namespace App\Shop\Carousels\Repositories;
use App\Shop\Carousels\Carousel;
use App\Shop\Carousels\Exceptions\CarouselNotFoundException;
use App\Shop\Carousels\Exceptions\CreateCarouselErrorException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
class CarouselRepository
{
   protected $model;

    /**
     * CarouselRepository constructor.
     * @param Carousel $carousel
     */
    public function __construct(Carousel $carousel)
    {
        $this->model = $carousel;
    }
    ...

     /**
     * @param array $data
     * @return bool
     * @throws UpdateCarouselErrorException
     */
    public function updateCarousel(array $data) : bool
    {
        try {
            return $this->model->update($data);
        } catch (QueryException $e) {
            throw new UpdateCarouselErrorException($e);
        }
    }
    ...
}

运行phpunit

PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 969 ms, Memory: 26.00MB
OK (1 test, 1 assertion)

非常好,大兄弟( 原文:Great dude! :) )

delete test

接下来是delete但是我们必须把deleteCarousel()方法的返回值类型声明从bool改为?bool意思是它可以返回boolean或者null

Note: 你必须运行在PHP7.1以上的环境才能应用上面的那个特性http://php.net/manual/en/migration71.new-features.php

<?php
namespace App\Shop\Carousels\Repositories;
use App\Shop\Carousels\Carousel;
use App\Shop\Carousels\Exceptions\CarouselNotFoundException;
use App\Shop\Carousels\Exceptions\CreateCarouselErrorException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
class CarouselRepository
{
   protected $model;

    /**
     * CarouselRepository constructor.
     * @param Carousel $carousel
     */
    public function __construct(Carousel $carousel)
    {
        $this->model = $carousel;
    }
    ...

    /**
    * @return bool
    */
    public function deleteCarousel() : ?bool
    {
        return $this->model->delete();
    }

然后是测试程序

<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
    /** @test */
    public function it_returns_null_when_deleting_a_non_existing_carousel()
    {
        $carouselRepo = new CarouselRepository(new Carousel);
        $delete = $carouselRepo->deleteCarousel();
        $this->assertNull($delete);
    }
    /** @test */
    public function it_should_throw_update_error_exception_when_the_carousel_has_failed_to_update()
    {
        $this->expectException(UpdateCarouselErrorException::class);
        $carousel = factory(Carousel::class)->create();
        $carouselRepo = new CarouselRepository($carousel);
        $data = ['title' => null];
        $carouselRepo->updateCarousel($data);
    }  

    /** @test */
    public function it_should_throw_not_found_error_exception_when_the_carousel_is_not_found()
    {
        $this->expectException(CarouselNotFoundException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->findCarousel(999);
    }
    /** @test */
    public function it_should_throw_an_error_when_the_required_columns_are_not_filled()
    {
        $this->expectException(CreateCarouselErrorException::class);
        $carouselRepo = new CarouselRepository(new Carousel);
        $carouselRepo->createCarousel([]);
    }
}

运行phpunit的结果如下:

➜  git: phpunit --filter=CarouselUnitTest::it_error_when_deleting_a_non_existing_carousel
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 938 ms, Memory: 26.00MB
OK (1 test, 1 assertion)

到这里关于怎么实现CRUD的反向单元测试的过程就讲完了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
公众号:网管叨bi叨 | Golang、Laravel、Docker、K8s等学习经验分享
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
113
粉丝
365
喜欢
484
收藏
314
排名:34
访问:20.4 万
私信
所有博文
社区赞助商