上一篇翻譯的文章裡通過簡單的十一步講述瞭如何在Laravel中進行測試驅動開發,這是作者關於測試的第二篇文章, 文章中簡述瞭如何在Laravel中進行增刪改查的單元測試,本文中的單元測試都是正向測試,之後還會有一篇來講述如何做反向測試。
正向測試: Positive test 提供合法資料,測試預期被測程式能得到正常執行。
反向測試:Negative test 提供非法的輸入資料,測試預期被測程式丟擲指定的Exception。
以下是譯文:
最近我開啟了一個開源電子商務應用專案LARACOM, 使用的是Laravel框架並整合了Rob Gloudeman 的shoping cart 和與其相關的packages。
在開啟這個專案時我必須為專案做長遠規劃和考慮,所以在我腦海中從來就沒出現過"我不會用到TDD(test driven development)方法"這個想法,TDD是開發的必選項。為了進行TDD,我需要將專案中的測試用例劃分到兩個不同的組中,一組是單元測試另外一組是功能測試。
單元測試是用來測試專案中的Model、Repository等等這些類的,而功能測試是看測試程式碼是否能夠正確訪問到Controller並斷言Controller的行為:是否redirect 到了目標URL、返回了指定的檢視或者是跳轉並攜帶著造成跳轉的錯誤資訊。
介紹的足夠多了,如果你之前沒有嘗試過TDD,我之前有寫進行TDD的基本方法。那篇文章裡有介紹TDD的基本概念和開始TDD的基本方法,所以這裡不再贅述。
今天我們要做的是為我的專案寫Carousel
業務的基本CRUD方法。
譯者注:文章裡作者開推薦了他寫的實現repository設計模式的package
Edit: Hey! I created a base repository package you can use for your next project :)
Part I: Positive Unit Testing
從create測試開始
讓我們從對create操作的測試開始。
建立/tests/Unit/Carousels/CarouselUnitTest.php
檔案
注:通過命令 php artisan make:test Carousels/CarouselUnitTest --unit
建立
<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
/** @test */
public function it_can_create_a_carousel()
{
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository(new Carousel);
$carousel = $carouselRepo->createCarousel($data);
$this->assertInstanceOf(Carousel::class, $carousel);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['image_src'], $carousel->src);
}
}
在這個檔案中我們要做的是:
- 測試respository類是否能夠用這些引數在資料庫中新建carousel記錄。
- 測試在新建carousel後,返回的carousel物件各屬性的值是否與建立時提供的引數值一致。
現在在你的終端中,執行vendor/bin/phpunit
(當前工作目錄必須是你專案的根目錄)。
是否有錯誤?是的,因為我們還沒有建立CarouselRepository.php
這個檔案,所以會有如下錯誤
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 700 ms, Memory: 26.00MB
There was 1 error:
1) Tests\Unit\Carousels\CarouselUnitTest::it_can_create_a_carousel
Error: Class 'Tests\Unit\Carousels\CarouselRepository' not found
讓我們建立app/Shop/Carousels/Repositories/CarouselRepository.php
檔案
<?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);
}
}
}
在這個repository中,我們用依賴注入將Carousel
Model注入到了其中,因為還沒有這個Carousel
Model所以在注入這個Model時會丟擲一個錯誤。
我們先來建立: app/Shop/Carousels/Carousel.php
<?php
namespace App\Shop\Carousels;
use Illuminate\Database\Eloquent\Model;
class Carousel extends Model
{
protected $fillable = [
'title',
'link',
'src'
];
}
在repository建立完成後,我們將其引入到我們的測試檔案中去,像這樣:
<?php
namespace Tests\Unit\Carousels;
use App\Shop\Carousels\Carousel;
use App\Shop\Carousels\Repositories\CarouselRepository;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
/** @test */
public function it_can_create_a_carousel()
{
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository(new Carousel);
$carousel = $carouselRepo->createCarousel($data);
$this->assertInstanceOf(Carousel::class, $carousel);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['image_src'], $carousel->src);
}
}
接下來在此執行vendor/bin/phpunit
Error Again? Yes? 你猜對了。
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 898 ms, Memory: 26.00MB
There was 1 error:
1) Tests\Unit\Carousels\CarouselUnitTest::it_can_create_a_carousel
App\Shop\Carousels\Exceptions\CreateCarouselErrorException: PDOException: SQLSTATE[HY000]: General error: 1 no such table: carousels in /Users/jsd/Code/shop/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:77
測試檔案中嘗試往carousels
表中寫入資料,但是這個表現在還不存在。我們接下來通過Migration檔案來建立這個表。
在命令列終端中執行:
php artisan make:migration create_carousels_table --create=carousels
編輯遷移檔案如下:
<?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');
}
}
link
欄位是可空的,title
和image
欄位是必須項。
執行遷移檔案建立完carousels
表後,再次執行vendor/bin/phpunit
顯示如下:
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 696 ms, Memory: 26.00MB
OK (1 test, 6 assertions)
現在你已經讓這個測試通過了,所以只要你在執行vendor/bin/phpunit
時這個測試能正確通過,那麼你就能認為會應用能成功建立carousel
記錄。
read測試
現在讓我們來測試在建立carousel
後,是否能從正確的讀取到它。
<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
/** @test */
public function it_can_show_the_carousel()
{
$carousel = factory(Carousel::class)->create();
$carouselRepo = new CarouselRepository(new Carousel);
$found = $carouselRepo->findCarousel($carousel->id);
$this->assertInstanceOf(Carousel::class, $found);
$this->assertEquals($found->title, $carousel->title);
$this->assertEquals($found->link, $carousel->link);
$this->assertEquals($found->src, $carousel->src);
}
/** @test */
public function it_can_create_a_carousel()
{
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository(new Carousel);
$carousel = $carouselRepo->createCarousel($data);
$this->assertInstanceOf(Carousel::class, $carousel);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['src'], $carousel->src);
}
}
執行測試,在每次我們新建測試後,我們總是預期測試會執行失敗,為什麼呢?因為我們還沒有實現邏輯。如果我們在建立完測試後執行既能得到success
的結果,那麼證明我們在應用TDD時步驟上出現了錯誤(TDD 先寫測試後編碼實現)。
每一個我們新建的測試都要放在測試檔案中的頭部,因為我們想讓新建的測試優先執行
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 688 ms, Memory: 26.00MB
There was 1 error:
1) Tests\Unit\Carousels\CarouselUnitTest::it_can_show_the_carousel
InvalidArgumentException: Unable to locate factory with name [default] [App\Shop\Carousels\Carousel].
在這個錯誤中顯示,測試程式嘗試呼叫了還不存在的模型工廠。
建立檔案: database/factories/CarouselModelFactory.php
<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/
use App\Shop\Carousels\Carousel;
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(Carousel::class, function (Faker\Generator $faker) {
return [
'title' => $faker->word,
'link' => $faker->url,
'src' => $faker->url,
];
});
在此執行phpunit
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
E 1 / 1 (100%)
Time: 708 ms, Memory: 26.00MB
There was 1 error:
1) Tests\Unit\Carousels\CarouselUnitTest::it_can_show_the_carousel
Error: Call to undefined method App\Shop\Carousels\Repositories\CarouselRepository::findCarousel()
現在找不到模型工廠的錯誤消失了,意味著現在可以持久化到資料庫裡了,有些人想讓建立物件和持久化的過程能夠分開,那麼可以將測試程式碼中的:
$carousel = factory(Carousel::class)->create();
替換成:
$carousel = factory(Carousel::class)->make();
但是現在測試程式中仍然有錯誤,因為在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 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);
}
}
/**
* @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);
}
}
}
現在執行phpunit
看看輸出是什麼。
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 932 ms, Memory: 26.00MB
OK (1 test, 6 assertions)
update測試
現在讓我們測試一下是否能夠對carousel
進行更新
<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
/** @test */
public function it_can_update_the_carousel()
{
$carousel = factory(Carousel::class)->create();
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository($carousel);
$update = $carouselRepo->updateCarousel($data);
$this->assertTrue($update);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['src'], $carousel->src);
}
/** @test */
public function it_can_show_the_carousel()
{
$carousel = factory(Carousel::class)->create();
$carouselRepo = new CarouselRepository(new Carousel);
$found = $carouselRepo->findCarousel($carousel->id);
$this->assertInstanceOf(Carousel::class, $found);
$this->assertEquals($found->title, $carousel->title);
$this->assertEquals($found->link, $carousel->link);
$this->assertEquals($found->src, $carousel->src);
}
/** @test */
public function it_can_create_a_carousel()
{
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository(new Carousel);
$carousel = $carouselRepo->createCarousel($data);
$this->assertInstanceOf(Carousel::class, $carousel);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['src'], $carousel->src);
}
}
這裡我們在吃使用模型工廠建立了carousel
記錄,然後通過$data
引數給updateCarousel
來更新carousel
並斷言更新後的carousel
物件的屬性值與$data
中的欄位值一樣。
現在這個測試會執行失敗,因為你知道的在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 Carousel
* @throws CreateCarouselErrorException
*/
public function createCarousel(array $data) : Carousel
{
try {
return $this->model->create($data);
} catch (QueryException $e) {
throw new CreateCarouselErrorException($e);
}
}
/**
* @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);
}
}
/**
* @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);
}
}
}
updateCarousel()
方法建立完後再次執行phpunit
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 932 ms, Memory: 26.00MB
OK (1 test, 6 assertions)
方法建立完後測試立馬就能執行成功了。
delete測試
最後讓我們看一下刪除carousel
測試
<?php
namespace Tests\Unit\Carousels;
use Tests\TestCase;
class CarouselUnitTest extends TestCase
{
/** @test */
public function it_can_delete_the_carousel()
{
$carousel = factory(Carousel::class)->create();
$carouselRepo = new CarouselRepository($carousel);
$delete = $carouselRepo->deleteCarousel();
$this->assertTrue($delete);
}
/** @test */
public function it_can_update_the_carousel()
{
$carousel = factory(Carousel::class)->create();
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository($carousel);
$update = $carouselRepo->updateCarousel($data);
$this->assertTrue($update);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['src'], $carousel->src);
}
/** @test */
public function it_can_show_the_carousel()
{
$carousel = factory(Carousel::class)->create();
$carouselRepo = new CarouselRepository(new Carousel);
$found = $carouselRepo->findCarousel($carousel->id);
$this->assertInstanceOf(Carousel::class, $found);
$this->assertEquals($found->title, $carousel->title);
$this->assertEquals($found->link, $carousel->link);
$this->assertEquals($found->src, $carousel->src);
}
/** @test */
public function it_can_create_a_carousel()
{
$data = [
'title' => $this->faker->word,
'link' => $this->faker->url,
'src' => $this->faker->url,
];
$carouselRepo = new CarouselRepository(new Carousel);
$carousel = $carouselRepo->createCarousel($data);
$this->assertInstanceOf(Carousel::class, $carousel);
$this->assertEquals($data['title'], $carousel->title);
$this->assertEquals($data['link'], $carousel->link);
$this->assertEquals($data['src'], $carousel->src);
}
}
然後在repository中建立deleteCarousel()
方法:
<?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 Carousel
* @throws CreateCarouselErrorException
*/
public function createCarousel(array $data) : Carousel
{
try {
return $this->model->create($data);
} catch (QueryException $e) {
throw new CreateCarouselErrorException($e);
}
}
/**
* @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);
}
}
/**
* @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);
}
}
/**
* @return bool
*/
public function deleteCarousel() : bool
{
return $this->model->delete();
}
}
當這個方法被執行後,它應返回布林值。如果刪除成功返回True
,否則返回null
。然後再次執行phpunit
➜ git: phpunit --filter=CarouselUnitTest::it_can_delete_the_carousel
PHPUnit 6.5.7 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 916 ms, Memory: 26.00MB
OK (1 test, 1 assertion)
WOW. 現在你已經大功告成了CONGRATULATIONS!
如果想看更多的測試Example可以我的專案的單元測試目錄。
下一部分將講述在Laravel中進行Negative Unit Testing。
本作品採用《CC 協議》,轉載必須註明作者和本文連結