Laravel 測試預設配置 SQLite 做為資料庫,並使用 In memory 的方式執行測試。但產品資料庫會使用的 SQL Function 不一定能在 SQLite 上使用,目前在嘗試幾個做法來解決測試無法通過特定資料庫的 SQL Function 問題。目前在 PostgreSQL 上使用 TO_CHAR 進行日期格式化。但 SQLite 並沒有相同的函式,但有相似的函式 strftime

Mock DB:raw()

第一個想到做法是模擬對象 DB:raw,當查詢不複雜,這個方式方便有效。但如果整個查詢會用到很多 DB Facade 的時候,每一個用到的地方都需要適當的模擬。也因此。 DB::connection()->enableQueryLog()DB::connection()->getQueryLog() 也無法使用,如此一來就失去查看 DB Query 的作用。

// Mock DB::raw
DB::shouldReceive('raw')->andReturn("strftime('%Y-%m-%d', created_at)");

Post::select(DB::raw("TO_CHAR(created_at, 'YYYY-MM-DD')"));

PDO::sqliteCreateFunction()

再來是透過 PDO::sqliteCreateFunction() 可以在查詢使用到特定的 SQL Function 的時候,利用 PHP 的回調函式作為 SQLite 的 User Defined Function。官方的手冊顯示這個方法還在實驗性階段,得斟酌使用。 第一次學到這個方法是在 Laravel 為SQLite 增加自定 Function 這邊文章提到。但實際使用過中主要問題是參數的不對稱。TO_CAHR 使用 YYYY-MM-DD 作為 20XX-XX-XX 顯示,但在 PHP 方面卻是 Y-m-d。為此會需要花額外功夫對應參數。

$connection = \DB::connection();
$connection->getPdo()->sqliteCreateFunction('TO_CHAR', fn($field, $format) => date('Y-m-d', strtotime($field)));

Post::select(DB::raw("TO_CHAR(created_at, 'YYYY-MM-DD')"));

if database connection is sqlite

最後一個方式在程式碼裡頭做判斷,可以說是為了測試而寫程式。缺點就是得在程式碼中埋入相對應的測試用程式碼。但同時也避免的上述方法的缺點。

$dateFormat = "TO_CHAR(created_at, 'YYYY-MM-DD')";
if ('sqlite' === config('database.default')) {
    $dateFormat = "strftime('%Y-%m-%d', created_at)";
}

Post::select(DB::raw($dateFormat));

總結

以使用順序而言, PDO::sqliteCreateFunction() 可以優先考量,如果沒有對稱性問題的方法,可以統一在 TestCase 定義。滿足後續測試使用到相同 SQL Function 的問題。 再者是 Mock DB:raw,在不複雜的情境可以模式對象解決。最後不行再進到程式碼編寫相對應的測試用程式碼。