PHPUnit 搭配 Selenium 之後,就能實際對瀏覽器做驗收測試,也能測試 JavaScript 與 AJAX。

Laravel 在 5.1 之後,提供了應用程式測試 (Application Testing),讓我們不用開啟瀏覽器,就可以直接對 route、controller 與 blade 進行驗收測試,且執行速度非常快,但也因為沒有開啟瀏覽器,所以無法對 JavaScript 與 AJAX 進行測試,若搭配了 Selenium,配合我們熟悉的 PHPUnit,就能對驗收測試加以自動化。

Motivation


Laravel 的應用程式測試非常好用,API 語意清楚,且提供 fluent 方式串接,不過 Seleinum 提供的又是另外一套 API 方式,若能將應用程式測試與 Selenium 測試整合成相同的 API,那就太棒了。

Version


macOS Sierra 10.12
Java 1.8.0_102
PHP 7.0.8
Laravel 5.3.18
PhpStorm 2016.2.2
PHPUnit 5.5.5
phpunit-selenium 3.0.2
Selenium Standalone Server 2.53.1
ChromeDriver 2.24
Chrome 53.0.2785.143 (64-bit)

測試種類簡介


  • 單元測試 : 針對單一 class 與 method 去做測試,此時會將其相依的 class 加以 mock,也稱為隔離測試,為粒度最小,速度最快的測試。
  • 整合測試 : 對相依的 class 不加以 mock,主要在測試各 class 整合的結果。
  • 驗收測試 : 以使用者角度對瀏覽器做測試,為速度最慢的測試。

驗收測試


應用程式測試

前身是 Jeffery Way 所寫的 laracasts/integrated 套件,運用 Symfony 所提供的底層元件,可直接對 GUI 做測試,在 Laravel 5.1 正式被整合進來。

優點 :

不需啟動瀏覽器,速度與單元測試一樣快。

缺點 :

僅能測試 form 與 submit,無法測試 JavaScript 與 AJAX。

Selenium 測試

能對瀏覽器的鍵盤滑鼠動作加以自動化。

優點 :

能測試 JavaScript 與 AJAX。

缺點 :

須啟動瀏覽器,速度較慢。

沒正式支援 PHP。

PHPUnit


JUnit 在 PHP 的實作,最多人使用的單元測試 framework,在 Laravel 已經內建,不用另外安裝。

Selenium


Laravel 並沒有辦法直接控制 Chrome,必須藉由 Selenium Server,但由於 Selenium 目前沒有直接支援 PHP,因此我們必須透過 phpunit-selenium 套件去控制 Selenium Server。

對於瀏覽器部分,Selenium 預設只支援 Firefox,必須再安裝 ChromeDriver 才能讓 Selenium 控制 Chrome。

稍後將會介紹如何安裝紫色部分的 phpunit-selenium 、Selenium Server 與 ChromeDriver。

安裝 Selenium 環境


安裝 PHP7

請先準備好 PHP 7 環境,本範例無法在 PHP 5.6 執行。1 1安裝 PHP 7,macOS 請參考 如何使用 MAMP PRO 開發 Larave?

安裝 Composer

Composer 是 modern PHP 的基石,負責 PHP 的套件管理與 autoloading。2 2關於 Composer,詳細請參考 范聖佑Composer 從入門到實戰

下載範例

本次範例的種子專案放在 GitHub,請先下載。

1
oomusou@mac:~$ git clone https://github.com/oomusou/Laravel53SeleniumPHPUnit_seed

重建 vendor 目錄

1
oomusou@mac:~$ cd Laravel53SeleniumPHPUnit_seed
oomusou@mac:~/Laravel53SeleniumPHPUnit_seed$ composer install

安裝 JDK

Selenium Server 是基於 Java 所開發,在執行 Selenium Server 之前,必須先安裝 JDK

選擇 Java SE Development Kit 8u102,根據平台下載不同的 JDK 版本。

執行剛下載的 jdk-8u102-macosx-x64.dmg,安裝 JDK 8 Update 102.pkg

Continue 繼續。

Install 開始安裝。

安裝完成。

1
oomusou@mac:~/$ java -version

檢查所安裝的 Java 版本。

若看到 1.8.0_102,則 JDK 安裝成功。

phpunit-selenium

phpunit/phpunit-selenium 是由 PHPUnit 官方所提供,讓我們可以在 PHP 與 Selenium Server 溝通,直接對瀏覽器做控制,並使用 PHPUnit 做 assertion,目前僅支援 Selenium Server 2.x。3 3GitHub Commit : 安裝 phpunit-selenium

1
oomusou@mac:~/MyProject$ composer require phpunit/phpunit-selenium --dev

在專案目錄下由 Composer 安裝 phpunit/phpunit-selenium,由於這是開發用的套件,請加上 --dev 參數。4 4種子專案已經先將 phpunit-selenium 事先裝好了。

Selenium Server

我們必須透過 Selenium 幫我們對瀏覽器下指令,因此必須先安裝 Selenium Server

目前 3.0 尚在 beta,且 phpunit-selenium 目前僅支援 Selenium 2.x,所以選擇 previous release 安裝以前的版本。

選擇 2.53,這是 Selenium 2.x 的最後版本。

下載 selenium-server-standalone-2.53.1.jar,這是 2.53 的最後一個版本。5 5種子專案已經先將 Selenium Server 事先裝好了。

selenium-server-standalone-2.53.1.jar 複製到 Laravel 專案的根目錄下。6 6GitHub Commit : 安裝 Selenium Server

ChromeDriver

Selenium 預設只支援 Firefox,若要使用 Chrome,需另外安裝 ChromeDriver

選擇目前最新版本下載。

根據平台下載不同的 ChromeDriver 版本。

zip 解開後,將 chromedriver 複製到 Laravel 專案的根目錄下。7 7GitHub Commit : 安裝 ChromeDriver

測試 Selenium 環境


最後要測試整個 Selenium 測試環境是否能正常啟動。

啟動 Web Server

1
oomusou@mac:~$ cd Laravel53SeleniumPHPUnit_seed
oomusou@mac:~/Laravel53SeleniumPHPUnit_seed$ php artisan serve

啟動 PHP 內建的 Web Server。

將啟動在 http://localhost:8000

啟動 Selenium

1
oomusou@mac:~$ cd Laravel53SeleniumPHPUnit_seed
oomusou@mac:~/Laravel53SeleniumPHPUnit_seed$ java -jar selenium-server-standalone-2.53.1.jar

原來的 Web Server 必須保持啟動,另外開一個 process 啟動 Selenium Server。

若能看到 Selenium Server is up and running,則 Selenium 已經正常啟動。

執行測試

1
oomusou@mac:~$ cd Laravel53SeleniumPHPUnit_seed
oomusou@mac:~/Laravel53SeleniumPHPUnit_seed$ vendor/bin/phpunit

原來的 Web Server 與 Selenium Server 必須保持啟動,再另外開一個 process 執行測試。

若能看到 2 個測試都是 綠燈,表示整個 Selenium 測試環境已經安裝成功。

安裝 PhpStorm


設定目錄

Sources
設定 PSR-0 namespace roots 的主目錄,也就是 Laravel 的 app 目錄。

PhpStorm -> Preferences -> Directories

選擇 app 目錄,按上方的 Sources

按上方的 Sources 之後,會在右側出現 Source Folders app

P 設定該目錄的 property。

Package prefix 輸入 App,因為 app 目錄對應的正是 Laravel 的 namespace App

這是所有 directories 設定中最重要的一個,在 Laravel 5 之後,全面使用 namespace,管理 namespace 成為很多人的惡夢,但只要設定了 Sources 之後,將來 PhpStorm 會幫我們管理 namespace。

Tests
設定測試程式的主目錄,也就是 Laravel 的 tests 目錄。

選擇 tests 目錄,按上方的 Tests

按上方的 Tests 之後,會在右側出現 Test Source Folders tests

Resource Root
設定前端資源的主目錄,也就是 Laravel 的 public 目錄。

選擇 public 目錄,按上方的 Resource Root。,會在右側出現 Resource roots public

按上方的 Resource Root 之後,會在右側出現 Resource roots public

設定字型

PhpStorm 的字型大小預設是 12,個人覺得稍微小了一點,會將字型稍微調大。
Editor

PhpStorm -> Preferences -> Editor -> Color & Fonts -> Font

建議將字型大小調成 15

IDE

PhpStorm -> Preferences -> Appearance & Behavior -> Appearance

勾選 Override default fonts by (not recommend),建議將字型大小改成 16

Terminal

PhpStorm -> Preferences -> Editor -> Colors & Fonts -> Console Font

在 PhpStorm 內可以直接在 terminal 下指令,建議將字型改成 168 8Terminal 字型必須重新啟動 PhpStorm 才會生效。

設定 PHP

PhpStorm 內建支援 PHP,我們可以直接在 PhpStorm 內執行 PHP。

PhpStorm -> Preferences -> Language & Frameworks -> PHP

  • PHP language level : 7 (return types, scalar type hints, etc.)

將 language level 調整為 7,讓 PhpStorm Code Inspection 自動針對 PHP 7 語法加以檢查。9 9關於 PhpStorm 的 Code Inspection,詳細請參考如何在 PhpStorm 使用 Code Inspection?

... 設定 PHP CLI 路徑。

PhpStorm 允許我們設定多組 PHP CLI 版本供切換。

  • PHP executable : /Applications/MAMP/bin/php/php7.0.8/bin/php
  • Debugger extension : /Applications/MAMP/bin/php/php7.0.8/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so

從 PhpStorm 2016.2 開始,提供了 Xdebug On Demand 功能,不必事先在 php.ini 啟動 Xdebug 影響 Composer 速度,只要事先設定好 xdebug.so 路徑,PhpStorm 會在設定中斷點除錯時,自動啟動 Xdebug。10 10關於 PhpStorm 的 Xdebug on Demand,詳細請參考如何在 PhpStorm 使用 Xdebug on Demand?

  • Interpreter : PHP7(7.0.8)

將 interpreter 設定為剛剛設定的 PHP CLI。

設定 PHPUnit

PhpStorm 內建支援 PHPUnit,我們可以直接在 PhpStorm 內直接執行 PHPUnit。

PhpStorm -> Preferences -> Language & Frameworks -> PHP -> PHPUnit

選擇 Use Composer autoloader :

  • Path to script : /Users/oomusou/Code/PHPConf/Laravel53SeleniumPHPUnit_demo/vendor/autoload.php

  • Default configuration file : /Users/oomusou/Code/PHPConf/Laravel53SeleniumPHPUnit_demo/phpunit.xml

Path to script 指定到專案目錄下的 vendor/autoload.php,告訴 PhpStorm 在執行 PHPUnit 時,先執行此路徑的 autoload。

Default configuration file 指定到專案根目錄下的 phpunit.xml,此為 PHPUnit 的設定檔,Laravel 已經在此檔設定好很多預設值。

設定 phpunit.xml

phpunit.xml 位於 Laravel 專案根目錄下,為 PHPUnit 設定檔,Laravel 已將幫我們做了很多設定。

執行測試時,Laravel 會先讀取 .env,再讀取 phpunit.xml,若有一些變數在執行測試階段設定,與原本 .env 設定不同,可以寫在 phpunit.xml
phpunit.xml11 11GitHub Commit : 設定 phpunit.xml

phpunit.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">

<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</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="BROWSER" value="chrome"/>
<env name="WEBSERVER_URL" value="localhost"/>
<env name="WEBSERVER_PORT" value="8000"/>
<env name="SELENIUM_URL" value="localhost"/>
<env name="SELENIUM_PORT" value="4444"/>
<env name="AJAX_DELAY" value="1"/>
</php>
</phpunit>

26 行

1
2
3
4
5
6
<env name="BROWSER" value="chrome"/>
<env name="WEBSERVER_URL" value="localhost"/>
<env name="WEBSERVER_PORT" value="8000"/>
<env name="SELENIUM_URL" value="localhost"/>
<env name="SELENIUM_PORT" value="4444"/>
<env name="AJAX_DELAY" value="1"/>

  • BROWSER : 設定要測試的瀏覽器,目前預設為 chrome,若日後要測試其他瀏覽器,只要安裝好該瀏覽器的 WebDriver,修改此設定即可。
  • WEBSERVER_URL : 設定 Web Server 的 URL,目前都在本機跑,所以為 localhost,將來配合 CI Server 時,Web Server 可能會在不同主機,修改此設定即可。
  • WEBSERVER_PORT : 設定 Web Server 的 port,目前都在本機跑,所以 port 為 8000,將來配合 CI Server 時,Web Server 的 port 可能不同,修改此設定即可。
  • SELENIUM_URL : 設定 Selenium Server 的 URL,目前都在本機跑,所以為 localhost,將來配合 CI Server 時,Selenium Server 可能會在不同主機,修改此設定即可。
  • SELENIUM_PORT : 設定 Selenium Server 的 port,預設 Selenium Server 使用 port 4444,將來配合 CI Server 時,Selenium Server 的 port 可能不同,修改此設定即可。
  • AJAX_DELAY : 使用 AJAX 時,需要等一段時間由 Web Server 回應,由於本機速度快,只需 1 秒即可,將來配合 CI Server 時,等待的時間可能不同,修改此設定即可。12 12種子專案已經先將 phpunit.xml 事先設定好了。

測試 PhpStorm 環境


當 PhpStorm 都設定好後,確認是否能直接在 PhpStorm 內跑測試。

將專案視窗的右上方只選擇 Tests,將只顯示 Laravel 的 tests 目錄。

選擇 tests 目錄,按滑鼠右鍵選擇 Run tests,或按熱鍵 ⌃ + ⇧ + R 執行測試。

若也能看到 2 個測試都是 綠燈,表示整個 PhpStorm 測試環境已經設定成功。

最簡單的驗收測試


應用程式測試

ExampleTest.php

tests/ExampleTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{

/**
* A basic functional test example.
*
* @return void
*/

public function testBasicExample()
{

$this->visit('/')
->see('Laravel');
}
}

每個 Laravel 都有這隻測試,用來測試 Laravel 預設首頁是否有 Laravel 字串。

  • TestCase : 每個 Laravel 的測試都要繼承 TestCase
  • Test Method : 每個 test method 必須以 test 開頭,或加上註解 @test,PHPUnit 才會執行。
  • visit() : 以 GET 方式對指定 URL 加以測試。
  • see() : 測試網頁內容是否包含指定字串。

TestCase.php

tests/TestCase.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class TestCase extends Illuminate\Foundation\Testing\TestCase
{

/**
* The base URL to use while testing the application.
*
* @var string
*/

protected $baseUrl = 'http://localhost';

/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/

public function createApplication()
{

$app = require __DIR__.'/../bootstrap/app.php';

$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

return $app;
}
}

為 Laravel 測試的 abstract class,主要目的在載入 Laravel kernel 部分。

Selenium 測試

ExampleSeleniumTest.php

tests/ExampleSeleniumTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
declare(strict_types = 1);

class ExampleSeleniumTest extends PHPUnit_Extensions_Selenium2TestCase
{

protected function setUp()
{

$this->setBrowser(env('BROWSER'));
$this->setBrowserUrl('http://' . env('WEBSERVER_URL') . ':' . env('WEBSERVER_PORT'));
$this->setHost(env('SELENIUM_URL'));
$this->setPort((int)env('SELENIUM_PORT'));
}

public function testTitle()
{

$this->url('/');
$this->assertEquals('Laravel', $this->title());
}
}

這是摘自於 phpunit-selenium 官網,為最簡單的 Selenium 測試。

不同於應用程式測試是繼承於 TestCase,Selenium 測試要改繼承 phpunit-selenium 所提供的 PHPUnit_Extensions_Selenium2TestCase

第 5 行

1
2
3
4
5
6
7
protected function setUp()
{

$this->setBrowser(env('BROWSER'));
$this->setBrowserUrl('http://' . env('WEBSERVER_URL') . ':' . env('WEBSERVER_PORT'));
$this->setHost(env('SELENIUM_URL'));
$this->setPort((int)env('SELENIUM_PORT'));
}

每個測試案例執行時,都會執行 setUp(),可用來執行設定動作。

  • setBrowser() : 設定要測試的瀏覽器。
  • setBrowserUrl() : 設定 Web Server 的 URL 與 port。
  • setHost() : 設定 Selenium Server 的 URL。
  • setPort() : 設定 Selenium Server 的 port。

13 行

1
2
3
4
5
public function testTitle()
{

$this->url('/');
$this->assertEquals('Laravel', $this->title());
}

  • url() : 以 GET 方式對指定 URL 加以測試,相當於 Laravel 應用程式測試的 visit()
  • assertEquals() : 測試網頁的 title 是否為 Laravel

建立 Todo 專案


以 Laravel 官方的 Basic Task List 為範例,建立一個簡單的 Todo 專案,將以此專案建立驗收測試 : 分別使用應用程式測試Selenium測試13 13GitHub Commit : 建立 Todo

一開始顯示目前所有的 task,因為還沒建立任何 task,所以是空的。

輸入 Study,按 Add Task 新增。

Study 被新增到資料庫,並顯示在下方的 Current Tasks

按下 Delete 刪除 Study

Study 從資料庫被刪除,不顯示任何 task。

驗收測試案例


目前 Todo 專案非常簡單,我們想為此專案建立 3 個測試案例。

  • Todo 一啟動時,顯示目前資料庫的 3 筆 task。
  • 新增 1 筆 task,並顯示在下方的 Current Tasks
  • 新增 1 筆 task,並立即刪除 task。

應用程式測試


1
oomusou@mac:~/MyProject$ php artisan make:test TodoAppTest

由 Laravel artisan 建立 TodoAppTest

測試案例1

Todo 一啟動時,顯示目前資料庫的 3 筆 task。

TodoAppTest.php14 14GitHub Commit : TodoAppTest (一啟動顯示 3 筆 task)

tests/TodoAppTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
declare(strict_types = 1);

use App\Task;

class TodoAppTest extends TestCase
{

/** @var string */
private $rootURL = 'todo';

protected function tearDown()
{

DB::table('tasks')->truncate();
parent::tearDown();
}

/** @test */
public function 一啟動顯示3筆task()
{

factory(Task::class)->create(['name' => 'Task 1']);
factory(Task::class)->create(['name' => 'Task 2']);
factory(Task::class)->create(['name' => 'Task 3']);

$this->visit($this->rootURL)
->see('Task 1')
->see('Task 2')
->see('Task 3')
->seeInDatabase('tasks', [
'name' => 'Task 1',
])
->seeInDatabase('tasks', [
'name' => 'Task 2',
])
->seeInDatabase('tasks', [
'name' => 'Task 3',
]);
}
}

10 行

1
2
3
4
5
protected function tearDown()
{

DB::table('tasks')->truncate();
parent::tearDown();
}

撰寫測試時,只要牽涉到資料庫讀寫,就必須遵造童子軍法則 : 測試結束時,要負責將假資料刪除,如此才能確保下一個測試執行時,是一個完全乾淨的資料庫,測試時才沒有任何 side effect。

每個測試案例執行完時,都會執行 tearDown(),所以可以將 truncate 動作寫在 tearDown() 內。

19 行

1
2
3
factory(Task::class)->create(['name' => 'Task 1']);
factory(Task::class)->create(['name' => 'Task 2']);
factory(Task::class)->create(['name' => 'Task 3']);

使用後端技術做驗收測試,最大的優點就是可以根據測試案例的需求,直接對資料庫做假資料。

這裡直接使用 Laravel 的 Model Factory。15 15關於 Model Factory,詳細請參考 Laravel 官網 Model Factories

23 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** @test */
$this->visit($this->rootURL)
->see('Task 1')
->see('Task 2')
->see('Task 3')
->seeInDatabase('tasks', [
'name' => 'Task 1',
])
->seeInDatabase('tasks', [
'name' => 'Task 2',
])
->seeInDatabase('tasks', [
'name' => 'Task 3',
]);

  • visit() : 以 GET 方式對指定 URL 加以測試。
  • see() : 測試網頁是否包含指定字串,相當於 PHPUnit 的 assertContains()
  • seeInDatabase() : 測試資料是否存在於資料庫,第 1 個參數為 table 名稱,第 2 個參數為欲驗收資料的陣列,key 為欄位名稱,value 為資料。

應用程式測試讓我們不僅可以測試網頁資料是否正確,還可以順便測試資料庫資料,也由於其 fluent API 特性,可讀性非常高,讓我們可以使用口語化的方式輕鬆閱讀測試程式碼,這也是應用程式測試最大的魅力所在。

一啟動顯示3筆task 的 method 內,按滑鼠右鍵選擇 Run TodoAppTest,一啟動顯示3筆task,或按熱鍵 ⌃ + ⇧ + R,只執行此測試案例。

測試會通過並顯示 綠燈,注意此時並沒有啟動瀏覽器,就如往常執行單元測試一樣,這也是應用程式測試速度超快的原因。

測試案例2

新增 1 筆 task,並顯示在下方的 Current Tasks

TodoAppTest.php16 16GitHub Commit : TodoAppTest (新增 1 筆 task 並顯示在下方)

tests/TodoAppTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
declare(strict_types = 1);

use App\Task;

class TodoAppTest extends TestCase
{

/** @test */
public function 新增1筆task並顯示在下方()
{

$this->visit($this->rootURL)
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);

$this->visit($this->rootURL)
->type('Task 1', 'name')
->press('Add Task')
->see('Task 1')
->seeInDatabase('tasks', [
'name' => 'Task 1'
]);
}
}

10 行

1
2
3
4
5
$this->visit($this->rootURL)
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);

前一個測試我們使用了 Model Factory 對資料庫新增假資料,特別在測試一開始,先確認資料已經被刪除。

  • dontSee() : 測試網頁是否包含指定字串,相當於 PHPUnit 的 assertNotContains()
  • dontSeeInDatabase() : 測試資料是否存在於資料庫。

16 行

1
2
3
4
5
6
7
$this->visit($this->rootURL)
->type('Task 1', 'name')
->press('Add Task')
->see('Task 1')
->seeInDatabase('tasks', [
'name' => 'Task 1'
]);

模擬使用者打字輸入的動作與按下 button,最後再測試資料是否已經新增到資料庫。

  • type() : 針對 <input type=text> 做輸入,第 1 個參數為欲輸入字串,第 2 個參數為 HTML 的 name。
  • press() : 按下 <input type=submit>,第 1 個參數為 button 顯示的字串。

測試會通過並顯示 綠燈,儘管這次有輸入的動作,依然沒有啟動瀏覽器。

測試案例3

新增 1 筆 task,並立即刪除 task。

TodoAppTest.php17 17GitHub Commit : TodoAppTest (新增 1 筆 task 立即刪除)

tests/TodoAppTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
declare(strict_types = 1);

use App\Task;

class TodoAppTest extends TestCase
{

/** @test */
public function 新增1筆task立即刪除()
{

$this->visit($this->rootURL)
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);

$this->visit($this->rootURL)
->type('Task 1', 'name')
->press('Add Task')
->see('Task 1')
->seeInDatabase('tasks', [
'name' => 'Task 1'
]);

$this->post($this->rootURL . '/task/1')
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);
}
}

24 行

1
2
3
4
5
$this->post($this->rootURL . '/task/1')
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);

目前每個 Delete 都有自己的 <form> 做 submit,但應用程式測試只能針對單一 <form> 做測試,因此無法使用 press() 來按 Delete,改用 post() URL 的方式。

測試會通過並顯示 綠燈,這次依然沒有啟動瀏覽器。

一次執行 TodoAppTest 的 3 個測試,都是 綠燈

建立 AJAX 版 Todo 專案


應用程式測試有很多優點,如執行速度快、API 語意清楚、fluent 方式串接,且還可同時測試資料庫,但由於測試時沒有啟動瀏覽器,因此無法測試 JavaScript 與 AJAX 部分,只能測試 <form> 與 submit,但實務上 JavaScript 與 AJAX 需求越來越多,顯然只有應用程式測試是不夠的。18 18GitHub Commit : 建立 AJAX 版 Todo

剛剛的 Todo 專案,Add Task 有一個 <form>,而每個 Delete 都有自己的 <form>,也就是整個網頁基本上都是透過 <form> 與 submit 機制達成,因此可以順利使用應用程式測試,現在我們要將 Add Task 與所有的 Delete 都改用 JavaScript 與 AJAX 方式。

Selenium 測試


1
oomusou@mac:~/MyProject$ php artisan make:test TodoSeleniumTest

由 Laravel artisan 建立 TodoSeleniumTest

將原來的 TodoAppTest 所有測試測試複製過來,只有 URL 從 /todo 改成 /todo2

使用應用程式測試跑 AJAX 版的 Todo 就悲劇了,只有第 1 個測試案例能通過,因為後 Add TaskDelete 都改用了 JavaScript 與 AJAX,這就必須動用 Selenium 測試了。

測試案例1

Todo 一啟動時,顯示目前資料庫的 3 筆 task。

TodoSeleniumTest.php19 19GitHub Commit : TodoSeleniumTest (一啟動顯示 3 筆 task)

tests/TodoSeleniumTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
declare(strict_types = 1);

use App\Task;

class TodoSeleniumTest extends PHPUnit_Extensions_Selenium2TestCase
{

/** @var string */
private $rootURL = 'todo2';
/** @var int */
private $ajaxDelay = 1;

protected function setUp()
{

parent::setUp();
$this->setBrowser(env('BROWSER'));
$this->setBrowserUrl('http://' . env('WEBSERVER_URL') . ':' . env('WEBSERVER_PORT'));
$this->setHost(env('SELENIUM_URL'));
$this->setPort((int)env('SELENIUM_PORT'));
$this->ajaxDelay = (int)env('AJAX_DELAY');

$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
}

protected function tearDown()
{

DB::table('tasks')->truncate();
parent::tearDown();
}

/** @test */
public function 一啟動顯示3筆task()
{

factory(Task::class)->create(['name' => 'Task 1']);
factory(Task::class)->create(['name' => 'Task 2']);
factory(Task::class)->create(['name' => 'Task 3']);

$this->url($this->rootURL);

$this->assertContains('Task 1', $this->byTag('body')->text());
$this->assertContains('Task 2', $this->byTag('body')->text());
$this->assertContains('Task 3', $this->byTag('body')->text());
}

}

第 5 行

1
class Todo2SeleniumTest extends PHPUnit_Extensions_Selenium2TestCase

改用 phpunit-selenium 驅動 Selenium,因次必須繼承 phpunit-selenium 所提供的 PHPUnit_Extensions_Selenium2TestCase

第 7 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** @var string */
private $rootURL = 'todo2';
/** @var int */
private $ajaxDelay = 1;

protected function setUp()
{

parent::setUp();
$this->setBrowser(env('BROWSER'));
$this->setBrowserUrl('http://' . env('WEBSERVER_URL') . ':' . env('WEBSERVER_PORT'));
$this->setHost(env('SELENIUM_URL'));
$this->setPort((int)env('SELENIUM_PORT'));
$this->ajaxDelay = (int)env('AJAX_DELAY');

$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
}

不同於應用程式測試,Selenium 測試必須在每個測試案例執行時,設定 Web Server URL、port、Selenium URL、port 等資訊。

除此之外,還要特別設定 $ajaxDelay 時間,好讓測試等 AJAX 回應,否則動態產生的 HTML 元素會抓不到。

最後在每個測試案例執行時,還必須載入 Laravel 核心部分,在應用程式測試時,我們不需要處理這些事情,因為 TestCase 幫我們做掉了,這裡因為改繼承 PHPUnit_Extensions_Selenium2TestCase,所以載入 Laravel 核心這件事情必須自己處理。

31 行

1
2
3
4
5
6
7
8
9
10
11
12
13
/** @test */
public function 一啟動顯示3筆task()
{

factory(Task::class)->create(['name' => 'Task 1']);
factory(Task::class)->create(['name' => 'Task 2']);
factory(Task::class)->create(['name' => 'Task 3']);

$this->url($this->rootURL);

$this->assertContains('Task 1', $this->byTag('body')->text());
$this->assertContains('Task 2', $this->byTag('body')->text());
$this->assertContains('Task 3', $this->byTag('body')->text());
}

一樣先使用 Model Factory 對資料庫新增 3 筆假資料。

  • url() : 為 phpunit-selenium 所提供的 API,會以 GET 方式對指定 URL 加以測試,相當於應用程式測試的 visit()
  • byTag() : phpunit-selenium 提供一系列 byXXX() 的 API,如 byId()byName()byTag()byXPath() …,讓我們可以抓到 HTML 的元素,進而加以控制。
  • assertContains() : PHPUnit 所提供的 API,測試是否包含指定字串,相當於應用程式測試的 see()

測試會通過並顯示 綠燈,與應用程式測試不同的時,此時會真的啟動瀏覽器,因此可以測試 JavaScript 與 AJAX,也因為必須啟動瀏覽器,所以測試速度較應用程式測試來得慢。

測試案例2

新增 1 筆 task,並顯示在下方的 Current Tasks

TodoSeleniumTest.php20 20GitHub Commit : TodoSeleniumTest (新增 1 筆 task 並顯示在下方)

tests/TodoSeleniumTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
declare(strict_types = 1);

use App\Task;

class Todo2SeleniumTest extends PHPUnit_Extensions_Selenium2TestCase
{

/** @test */
public function 新增1筆task並顯示在下方()
{

$this->url($this->rootURL);
$this->assertNotContains('Task 1', $this->byTag('body')->text());

$this->url($this->rootURL);
$this->byName('name')->value('Task 1');
$this->byXPath("//button[contains(text(), 'Add Task')]")->click();

sleep($this->ajaxDelay);

$this->assertContains('Task 1', $this->byTag('body')->text());
}
}

第 7 行

1
2
$this->url($this->rootURL);
$this->assertNotContains('Task 1', $this->byTag('body')->text());

前一個測試我們使用了 Model Factory 對資料庫新增假資料,特別在測試一開始,先確認資料已經被刪除。

  • assertNotContains() : PHPUnit 所提供的 API,測試是否包含指定字串,相當於應用程式測試的 dontSee()

13 行

1
2
3
$this->url($this->rootURL);
$this->byName('name')->value('Task 1');
$this->byXPath("//button[contains(text(), 'Add Task')]")->click();

模擬使用者打字輸入的動作與按下 button。

  • byName() : phpunit-selenium 提供的 API,讓我們可以根據 name 抓到 HTML 的元素,進而加以控制。
  • byXPath() : phpunit-selenium 提供的 API,讓我們可以根據 XPath 抓到 HTML 的元素,進而加以控制。

17 行

1
sleep($this->ajaxDelay);

由於採用 AJAX 方式,需要等 server 回應,需要等一段時間,若在本機執行,實務上設定 1 秒即可,若要在 CI Server 上執行,時間需要設定長一點。

19 行

1
$this->assertContains('Task 1', $this->byTag('body')->text());

最後測試畫面是是否看得到 Task 1

測試會通過並顯示 綠燈,會看到瀏覽器啟動與實際輸入的動作。

測試案例3

新增 1 筆 task,並立即刪除 task。

TodoSeleniumTest.php21 21GitHub Commit : TodoSeleniumTest (新增 1 筆 task 立即刪除)

tests/TodoSeleniumTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
declare(strict_types = 1);

use App\Task;

class Todo2SeleniumTest extends PHPUnit_Extensions_Selenium2TestCase
{

/** @test */
public function 新增1筆task立即刪除()
{

$this->url($this->rootURL);
$this->assertNotContains('Task 1', $this->byTag('body')->text());

$this->url($this->rootURL);
$this->byName('name')->value('Task 1');
$this->byXPath("//button[contains(text(), 'Add Task')]")->click();

sleep($this->ajaxDelay);

$this->assertContains('Task 1', $this->byTag('body')->text());

$this->byXPath("//button[contains(text(), 'Delete')]")->click();

sleep($this->ajaxDelay);

$this->assertNotContains('Task 1', $this->byTag('body')->text());
}
}

21 行

1
$this->byXPath("//button[contains(text(), 'Delete')]")->click();

使用 byXPath() 找到 Delete,並模擬使用者按下 Delete 動作。

測試會通過並顯示 綠燈,會看到瀏覽器啟動與實際輸入,最後會看到實際刪除的動作。

一次執行 TodoSeleniumTest 的 3 個測試,都是 綠燈

將兩種測試 API 統一


雖然直接使用 phpunit-selenium 所提供的 API,我們已經能驅動 Selenium 並對 JavaScript 與 AJAX 加以測試,但相對於 Laravel 的應用程式測試,仍然有改善的空間 :

  • phpunit-selenium 的 API 與 應用程式 API 不同,也就是同樣寫驗收測試,必須學習兩套 API。
  • phpunit-selenium API 的語意較差,可讀性較差,將來維護不若應用程式測試般直覺。
  • phpunit-selenium 無法直接測試資料庫。

其實我們已經發現 PHPUnit 與 phpunit-selenium 所提供的 API,與應用程式測試的 API 很多功能是相同的,只是 API 不同而已,我們將學習 Laravel 的 TestCase,另外再包一個 TestCaseSelenium,將應用程式測試 API 實作於此,將來測試案例將不繼承 PHPUnit_Extensions_Selenium2TestCase,而改繼承我們自己的 TestCaseSelenium,就能達到應用程式測試與 Selenium 測試完全相同 API 的目標。

TestCaseSelenium.php22 22GitHub Commit : 建立 TestCaseSelenium

tests/TestCaseSelenium.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
declare(strict_types = 1);

use Illuminate\Contracts\Console\Kernel;

abstract class TestCaseSelenium extends PHPUnit_Extensions_Selenium2TestCase
{

protected $ajaxDelay = 1;

protected function setUp()
{

parent::setUp();
$this->setBrowser(env('BROWSER'));
$this->setBrowserUrl('http://' . env('WEBSERVER_URL') . ':' . env('WEBSERVER_PORT'));
$this->setHost(env('SELENIUM_URL'));
$this->setPort((int)env('SELENIUM_PORT'));
$this->ajaxDelay = (int)env('AJAX_DELAY');

$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
}

/**
* @param string $path
* @return TestCaseSelenium
*/

public function visit(string $path) : TestCaseSelenium
{

$this->url($path);

return $this;
}

/**
* @param string $text
* @param string $tag
* @return TestCaseSelenium
*/

public function see(string $text, string $tag = 'body') : TestCaseSelenium
{

$this->assertContains($text, $this->byTag($tag)->text());

return $this;
}

/**
* @param string $text
* @param string $tag
* @return TestCaseSelenium
*/

public function dontSee(string $text, string $tag = 'body') : TestCaseSelenium
{

$this->assertNotContains($text, $this->byTag($tag)->text());

return $this;
}

/**
* @param string $value
* @param string $name
* @return TestCaseSelenium
*/

public function type(string $value, string $name) : TestCaseSelenium
{

$this->byName($name)->value($value);

return $this;
}

/**
* @param $text
* @return TestCaseSelenium $this
*/

public function press(string $text) : TestCaseSelenium
{

$this->byXPath("//button[contains(text(), '{$text}')]")->click();
$this->hold($this->ajaxDelay);

return $this;
}

/**
* @param $seconds
* @return TestCaseSelenium $this
*/

public function hold(int $seconds) : TestCaseSelenium
{

sleep($seconds);

return $this;
}

/**
* @param $table
* @param array $data
* @param null $connection
* @return $this
*/

public function seeInDatabase($table, array $data, $connection = null)
{

$database = App::make('db');

$connection = $connection ?: $database->getDefaultConnection();

$count = $database->connection($connection)->table($table)->where($data)->count();

$this->assertGreaterThan(0, $count, sprintf(
'Unable to find row in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));

return $this;
}

/**
* @param string $table
* @param array $data
* @param string $connection
* @return $this
*/

protected function dontSeeInDatabase($table, array $data, $connection = null)
{

return $this->notSeeInDatabase($table, $data, $connection);
}

/**
* @param string $table
* @param array $data
* @param string $connection
* @return $this
*/

protected function notSeeInDatabase($table, array $data, $connection = null)
{


$database = App::make('db');

$connection = $connection ?: $database->getDefaultConnection();

$count = $database->connection($connection)->table($table)->where($data)->count();

$this->assertEquals(0, $count, sprintf(
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));

return $this;
}
}

第 5 行

1
abstract class TestCaseSelenium extends PHPUnit_Extensions_Selenium2TestCase

由我們自己建立的 TestCaseSelenium 去繼承 phpunit-selenium 的 PHPUnit_Extensions_Selenium2TestCase

第 7 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected $ajaxDelay = 1;

protected function setUp()
{

parent::setUp();
$this->setBrowser(env('BROWSER'));
$this->setBrowserUrl('http://' . env('WEBSERVER_URL') . ':' . env('WEBSERVER_PORT'));
$this->setHost(env('SELENIUM_URL'));
$this->setPort((int)env('SELENIUM_PORT'));
$this->ajaxDelay = (int)env('AJAX_DELAY');

$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
}

TodoSeleniumTest 中,這些設定都是寫在每個 test 的 setUp(),由於這些基礎設定都一樣,將其 Pull members upTestCaseSelenium,以後每個 test 就不需再寫了。

22 行

1
2
3
4
5
6
7
8
9
10
/**
* @param string $path
* @return TestCaseSelenium
*/

public function visit(string $path) : TestCaseSelenium
{

$this->url($path);

return $this;
}

應用程式測試的 visit() 其實相當於 phpunit-selenium 的 url()

33 行

1
2
3
4
5
6
7
8
9
10
11
/**
* @param string $text
* @param string $tag
* @return TestCaseSelenium
*/

public function see(string $text, string $tag = 'body') : TestCaseSelenium
{

$this->assertContains($text, $this->byTag($tag)->text());

return $this;
}

應用程式測試的 see() 其實相當於 PHPUnit 的 assertContains(),若沒有特別指定 tag,相當於從 body 下開始找。

45 行

1
2
3
4
5
6
7
8
9
10
11
/**
* @param string $text
* @param string $tag
* @return TestCaseSelenium
*/

public function dontSee(string $text, string $tag = 'body') : TestCaseSelenium
{

$this->assertNotContains($text, $this->byTag($tag)->text());

return $this;
}

應用程式測試的 dontSee() 其實相當於 PHPUnit 的 assertNotContains(),若沒有特別指定 tag,相當於從 body 下開始找。

57 行

1
2
3
4
5
6
7
8
9
10
11
/**
* @param string $value
* @param string $name
* @return TestCaseSelenium
*/

public function type(string $value, string $name) : TestCaseSelenium
{

$this->byName($name)->value($value);

return $this;
}

應用程式測試的 type() 其實相當於 phpunit-selenium 的 byName() 找到元素後,再用 value() 去填值。

69 行

1
2
3
4
5
6
7
8
9
10
11
/**
* @param $text
* @return TestCaseSelenium $this
*/

public function press(string $text) : TestCaseSelenium
{

$this->byXPath("//button[contains(text(), '{$text}')]")->click();
$this->hold($this->ajaxDelay);

return $this;
}

應用程式測試的 press() 其實相當於 phpunit-selenium 的 byXPath() 找到元素後,再用 click() 去按下 button。

由於 button 會搭配 AJAX,故需要 hold() 等待一點時間。

80 行

1
2
3
4
5
6
7
8
9
10
/**
* @param $seconds
* @return TestCaseSelenium $this
*/

public function hold(int $seconds) : TestCaseSelenium
{

sleep($seconds);

return $this;
}

應用程式測試的 hold() 其實相當於 sleep(),目的在等 AJAX 回應。

91 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @param $table
* @param array $data
* @param null $connection
* @return $this
*/

public function seeInDatabase($table, array $data, $connection = null)
{

$database = App::make('db');

$connection = $connection ?: $database->getDefaultConnection();

$count = $database->connection($connection)->table($table)->where($data)->count();

$this->assertGreaterThan(0, $count, sprintf(
'Unable to find row in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));

return $this;
}

此段程式是由 Laravel 的 Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase trait 移植過來的的,目的在測試資料是否在資料庫內。

112 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @param string $table
* @param array $data
* @param string $connection
* @return $this
*/

protected function dontSeeInDatabase($table, array $data, $connection = null)
{

return $this->notSeeInDatabase($table, $data, $connection);
}

/**
* @param string $table
* @param array $data
* @param string $connection
* @return $this
*/

protected function notSeeInDatabase($table, array $data, $connection = null)
{

$database = App::make('db');

$connection = $connection ?: $database->getDefaultConnection();

$count = $database->connection($connection)->table($table)->where($data)->count();

$this->assertEquals(0, $count, sprintf(
'Found unexpected records in database table [%s] that matched attributes [%s].', $table, json_encode($data)
));

return $this;
}

此段程式是由 Laravel 的 Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase trait 移植過來的的,目的在測試資料是否在資料庫內。

composer.json23 23GitHub Commit : 修改 composer.json

composer.json
1
2
3
4
5
6
"autoload-dev": {
"classmap": [
"tests/TestCase.php",
"tests/TestCaseSelenium.php"
]
},

TestCaseSelenium 為自己新建的 class,因為不是遵循 PSR-4 放在 app 目錄下,所以必須在 classmap 下加上 tests/TestCaseSelenium.php,讓 Composer 可以順利載入。

1
oomusou@mac:~/MyProject$ composer dumpautoload

重新命令 Composer 建立 autoload。

1
oomusou@mac:~/MyProject$ php artisan make:test TodoSeleniumAppTest

由 Laravel artisan 建立 TodoSeleniumAppTest

測試案例1

Todo 一啟動時,顯示目前資料庫的 3 筆 task。

TodoSeleniumAppTest.php24 24GitHub Commit : TodoSeleniumAppTest (一啟動顯示 3 筆 task)

tests/TodoSeleniumAppTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
declare(strict_types = 1);

use App\Task;

class Todo2SeleniumAppTest extends TestCaseSelenium
{

/** @var string */
private $rootURL = 'todo2';

protected function tearDown()
{

DB::table('tasks')->truncate();
parent::tearDown();
}

/** @test */
public function 一啟動顯示3筆task()
{

factory(Task::class)->create(['name' => 'Task 1']);
factory(Task::class)->create(['name' => 'Task 2']);
factory(Task::class)->create(['name' => 'Task 3']);

$this->visit($this->rootURL)
->see('Task 1')
->see('Task 2')
->see('Task 3')
->seeInDatabase('tasks', [
'name' => 'Task 1',
])
->seeInDatabase('tasks', [
'name' => 'Task 2',
])
->seeInDatabase('tasks', [
'name' => 'Task 3',
]);
}

}

從原來繼承 PHPUnit_Extensions_Selenium2TestCase 改繼承自己的 TestCaseSelenium,因為 API 與應用程式測試的 API 完全一樣,只要將原來 TodoAppTest一啟動顯示3筆task() 內的測試程式碼複製過來,再將 rootURL/todo 改成 /todo2 即可。

測試會通過並顯示 綠燈

測試案例2

新增 1 筆 task,並顯示在下方的 Current Tasks

TodoSeleniumAppTest.php25 25GitHub Commit : TodoSeleniumAppTest (新增 1 筆 task 並顯示在下方)

tests/TodoSeleniumAppTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
declare(strict_types = 1);

use App\Task;

class Todo2SeleniumAppTest extends TestCaseSelenium
{

/** @test */
public function 新增1筆task並顯示在下方()
{

$this->visit($this->rootURL)
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);

$this->visit($this->rootURL)
->type('Task 1', 'name')
->press('Add Task')
->hold($this->ajaxDelay)
->see('Task 1')
->seeInDatabase('tasks', [
'name' => 'Task 1'
]);

}
}

將原來 TodoAppTest新增1筆task並顯示在下方() 內的測試程式碼複製過來即可。

測試會通過並顯示 綠燈

測試案例3

新增 1 筆 task,並立即刪除 task。

TodoSeleniumAppTest.php26 26GitHub Commit : TodoSeleniumAppTest (新增 1 筆 task 立即刪除)

tests/TodoSeleniumAppTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
declare(strict_types = 1);

use App\Task;

class Todo2SeleniumAppTest extends TestCaseSelenium
{

/** @test */
public function 新增1筆task立即刪除()
{

$this->visit($this->rootURL)
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);

$this->visit($this->rootURL)
->type('Task 1', 'name')
->press('Add Task')
->hold($this->ajaxDelay)
->see('Task 1')
->seeInDatabase('tasks', [
'name' => 'Task 1'
]);

$this->press('Delete')
->hold($this->ajaxDelay)
->dontSee('Task 1')
->dontSeeInDatabase('tasks', [
'name' => 'Task 1'
]);
}
}

將原來 TodoAppTest新增1筆task立即刪除() 內的測試程式碼複製過來,唯一修改在於 Delete 部分,之前是直接 post URL,現在改成 press('Delete'),並加上hold($this->ajaxDelay)

測試會通過並顯示 綠燈

一次執行 TodoSeleniumAppTest 的 3 個測試,都是 綠燈

最後將測試全部跑一遍,全部都是 綠燈,包含之前寫的應用程式測試與 Selenium 測試。

Conclusion


  • 應用程式測試適合用在單純使用 <form> 來 submit 的需求,優點是速度非常快。
  • Selenium 測試適合用在使用 JavaScript 與 AJAX 的需求,由於需要開啟瀏覽器,速度稍慢。
  • 透過繼承 TestCaseSelenium,Selenium 測試也能使用與應用程式測試相同的 API,除了測試程式碼可無痛升級外,也能享受 API 語意清楚,fluent 方式串接的優點,還可以同時測試資料庫。
  • 目前 TestCaseSelenium 並沒有將應用程式測試的所有 API 包進來,不過證明是個可行的方式,當所有 API 都包進來後,預計將來會以 package 方式釋出。

Sample Code


完整的範例可以在我的 GitHub 上找到。

Reference


Taylor Otwell, Laravel Application Testing
Sebastian Bergmann, PHPUnit
Sebastian Bergmann, phpunit/phpunit-selenium
SeleniumHQ, Selenium Server
Google, ChromeDriver
Jeffery Way, Laracast : Laravel Test Helpers for Selenium