Laravel 測試驅動開發 -- 正向單元測試

KevinYan發表於2018-07-25

上一篇翻譯的文章裡通過簡單的十一步講述瞭如何在Laravel中進行測試驅動開發,這是作者關於測試的第二篇文章, 文章中簡述瞭如何在Laravel中進行增刪改查的單元測試,本文中的單元測試都是正向測試,之後還會有一篇來講述如何做反向測試。

正向測試: Positive test 提供合法資料,測試預期被測程式能得到正常執行。

反向測試:Negative test 提供非法的輸入資料,測試預期被測程式丟擲指定的Exception。

譯文連結:https://medium.com/@jsdecena/crud-unit-tes...

以下是譯文:

最近我開啟了一個開源電子商務應用專案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中,我們用依賴注入將CarouselModel注入到了其中,因為還沒有這個CarouselModel所以在注入這個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欄位是可空的,titleimage欄位是必須項。

執行遷移檔案建立完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 協議》,轉載必須註明作者和本文連結

公眾號:網管叨bi叨 | Golang、PHP、Laravel、Docker等學習經驗分享

相關文章