Laravel测试驱动开发--反向单元测试
Negative CRUD Unit Testing in Laravel5
这是一篇译文,原文链接: https://medium.com/@jsdecena/...
作为CRUD Unit Testing in Laravel5的第二部分,在这篇文章中我们将来讨论反向测试。
上一篇我们写的斗士正向测试;断言可以create
、update
、show
或者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
字段设置为可空,而title
和src
字段设置成了不允许为空。
<?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'); } }
所以我们预期当尝试设置title
和src
为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已经有值了。
来看一下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
<?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的反向单元测试的过程就讲完了。