優雅的 Expect API

斷定 (Assert) 是測試中最關鍵的部分,用來確定我們想測試的流程是否符合我們的預期

一般我們斷定一個值時,會使用 PHPUnit 提供的各種 assert 方法

$this->assertEqual($name, 'Allen');

Pest 提供一種更為優雅且更為清楚的斷定 (assert) 值類型 API:expect()

expect(true)->toBeTrue();

使用剛剛的新增聯絡人範例,看看 expect() 提供哪些方法

use function Pest\Faker\faker;

it('can store a contact', function () {
    login()->post('/contacts', [
        'first_name' => faker()->firstName,
        'last_name' => faker()->lastName,
        'email' => faker()->email,
        'phone' => faker()->e164PhoneNumber,
        'address' => 'No. 23, Fake Rd',
        'city' => 'Fake City',
        'region' => 'Fake Region',
        'country' => faker()->randomElement(['us', 'tw']),
        'postal_code' => faker()->postcode,
    ])
    ->assertRedirect('/contacts')
    ->assertSessionHas('success', 'Contact created');

    $contact = Contact::latest()->first();

    expect($contact->first_name)->toBeString(); // pass
    expect($contact->first_name)->toBeEmpty(); // fail
    expect($contact->first_name)->toBeString()->not->toBeEmpty(); // pass
    expect($contact->email)->toBeString()->toContain('@'); // pass
    expect($contact->phone)->toBeString()->toStartWith('+'); // pass
    expect($contact->address)->toBe('No. 23, Fake Rd'); // pass
    expect($contact->country)->toBeIn(['us', 'tw']); // pass
});

定義自己的 Expect 的斷定方法

在之前的 expect() 範例中,我們使用下方的方式來判斷一個信箱地址的格式是否正確

expect($contact->email)->toBeString()->toContain('@');

但這個方式直觀來看,很難第一眼了解這麼做的目的是什麼!

Pest 提供為 expect() 客製方法的功能,我們可以在 tests/Pest.php 中為 expect() 加上新的方法

expect()->extend('toBeEmailAddress', function () {
    expect($this->value)->toBeString()->toContain('@');
});

之後我們就可以在測試中使用 toBeEmailAddress()

expect($contact->email)->toBeEmailAddress();

不過上面的判斷方法其實還有很多漏洞,以下面的字串為例

fakeEmail@

這個字串很明顯就不是正常的信箱地址,但是確實可以通過我們上面所設定的斷定規則

為了更精確的判斷,我們可以在 expect() 的客製方法中使用 PHP 的一些方法來進行判斷,如果錯誤就拋出例外 (Exception)

請注意每個測試中,至少要有一個斷定

expect()->extend('toBeEmailAddress', function () {
    // 至少放一個斷定在方法中,避免單獨使用此方法導致測試中沒有斷定的錯誤
    expect($this->value)->toBeString();

    if (! (bool) filter_var($this->value, FILTER_VALIDATE_EMAIL)) {
        throw new ExpectationFailedException('Email address is not valid');
    }
});

Expect 的簡潔寫法

在剛剛新增聯絡人的例子中,我們用了很多斷定去確認 ORM Model 中的值

expect($contact->first_name)->toBeString(); // pass
expect($contact->first_name)->toBeEmpty(); // fail
expect($contact->first_name)->toBeString()->not->toBeEmpty(); // pass
expect($contact->email)->toEmailAddress(); // pass
expect($contact->phone)->toBeString()->toStartWith('+'); // pass
expect($contact->address)->toBe('No. 23, Fake Rd'); // pass
expect($contact->country)->toBeIn(['us', 'tw']); // pass

這裡其實可以使用 expect() 提供的 and() 方法,將多次斷定串連在一起

expect($contact->first_name)->toBeString() // pass
    ->and($contact->first_name)->toBeEmpty() // fail
    ->and($contact->first_name)->toBeString()->not->toBeEmpty() // pass
    ->and($contact->email)->toEmailAddress() // pass
    ->and($contact->phone)->toBeString()->toStartWith('+') // pass
    ->and($contact->address)->toBe('No. 23, Fake Rd') // pass
    ->and($contact->country)->toBeIn(['us', 'tw']); // pass

除了 and() 方法,Pest 還提供另外一種更簡潔的寫法

expect($contact)
    ->first_name->toBeString() // pass
    ->last_name->toBeEmpty() // fail
    ->first_name->toBeString()->not->toBeEmpty() // pass
    ->email->toEmailAddress() // pass
    ->phone->toBeString()->toStartWith('+') // pass
    ->address->toBe('No. 23, Fake Rd') // pass
    ->country->toBeIn(['us', 'tw']); // pass

需要注意的事情,Pest 無法為特定名稱生成 Expectation 物件,如果你的屬性名稱為 value,那麼這種簡潔的寫法就會失效

Pest 會檢查傳入的物件是否具有該屬性,如果有的話就會生成與其屬性同名稱的 Expectation 物件,讓你可以對其使用各種斷定方法,是不是很方便呢?

參考資料


This site uses Just the Docs, a documentation theme for Jekyll.