PHPでテスティングフレームワークを実装する前に知っておきたい勘所

公開日:

Download PDF

スライドテキスト

Page 1

テスティングフレ…ry

PHP
実装する前に知っておきたい勘所

Tips for implementing Testing Framework in PHP.

2018-03-09 PHPerKaigi 2018 前夜祭

Nerima Coconeri Hall #phperkaigi

Page 2

お前誰よ
うさみけんた (@tadsan) / Zonu.EXE

  • GitHub/Packagistでは id: zonuexe
    • ピクシブ株式会社技術基盤チーム (pixiv.net)
  • Emacs Lisper, PHPer
  • Emacs PHP Modeのメンテナ引き継ぎました
    • 好きなリスプはEmacs Lispです
    • Qiitaに記事を書いたり変なコメントしてるよ

Page 3

Page 4

pixivのコードは

毎日どんどん
刷新されてるよ

Page 5

明日のpixiv
作るのは俺たちだ

Page 6

We are hiring!

Page 7

アジェンダ

Page 8

本日しない話

Page 9

オブジェクト
指向

Page 10

TDD/BDDの 教義的な話

Page 11

定義に従った
厳密な用語

Page 12

Emacs
(私はphpunit.elの開発者でもあります)

Page 13

!

Page 14

一連のストーリー
などはない

Page 15

オムニバス

Page 16

1. フレームワークなしのテスト
2. できる! フレームワーク 3. PHPで注意すべき「状態」

Page 17

1. フレームワークなしのテスト
2. できる! フレームワーク 3. PHPで注意すべき「状態」 ↑時間なくて収まらなかった

Page 18

本題の前に

Page 19

Page 20

pixivは主に
PHPUnitでテスト

Page 21

僕がテストを新規を増や すならPHPUnitでテスト

Page 22

% cd ~/pixiv/tests

% git ls-files '*Test.php' | wc -l

1606

% git ls-files '*Test.php' | xargs cat | wc -l

Page 23

どうすれば書きやすく

メンテできる
テストが書けるか悩み

Page 24

本題の前に

Page 25

さて

Page 26

まれによく聞く

Page 27

PHPUnitとかphpspecとか 巨大な依存関係入れたくない

Page 28

せやな

Page 29

懸念の妥当性はともかく

依存パッケージの数が
それなりに多いのはたしか

Page 30

「簡単なテストだけ  できればいいよ」

Page 31

おう

Page 32

じゃ、作るか

Page 33

その前に

Page 34

ところでPHP標準のテス ティングフレームワーク
がある

Page 35

http://qa.php.net/write-test.php

Page 36

PHP本体の
テストコード

Page 37

php-srcに入ってる

run-tests.php

実行できる

Page 38

実はPHPUnitでも おまけ機能として テスト実行できる

Page 39

バッファの出力をテストする

--TEST--

--TEST--

GET $_ENV test

Compute 1 + 1 test

--ENV--

--FILE--

return <<<END

<?= 1 + 1 ?>

a=AAA

--EXPECT--

b=BBB

2

END;
--FILE--
<?php
echo getenv('a'), "\n"; echo getenv('b'), "\n";
?>
--EXPECT--
AAA BBB

Page 40

ただし

(意図的なのか)

仕様を完全には
サポートしてません

Page 41

もしphptを採用する

run-tests.php

なら
を使った方がいい

Page 42

テスト
スクリプト

Page 43

なぜ人は
テストを書くのか

Page 44

実装前に入出力の
例を明示したい

Page 45

機能追加や

リファクタリングで
意図した挙動が壊れな
いことを保障したい

Page 46

テストを書こう

Page 47

そうすると

Page 48

関心がどうとか
って話になってくる

Page 49

うるせえ

Page 50

やりたいことは
動作確認

Page 51

それだけ

Page 52

テストスクリプトを
書こう

Page 53

test.php

<?php
require_once __DIR__ . '/vendor/autoload.php';

(2 === 1 + 1) or die("LINE: ". __LINE__);
(3 === 1.5 + 1.5) or die("LINE: ". __LINE__);

$date = date('Y-m-d',4502304000);
("2112-09-13" === $date) or die("LINE: ". __LINE__);

$a = [1, 2, 3];
(1 == array_shift($a)) or die("LINE: ". __LINE__);

echo 'ok.', PHP_EOL;

Page 54

失敗すると行が 出力されて終了

Page 55

or die()
書くのだるい

Page 56

もうちょっと わかりやすく

Page 57

assert()

Page 58

test.php

#!/usr/bin/env php
<?php
require_once __DIR__ . '/vendor/autoload.php';

assert(2 === 1 + 1);
assert(3 === 1.5 + 1.5);
assert("2112-09-13" === date('Y-m-d', 4503168000));

$a = [1, 2, 3];
assert(1 == array_shift($a));

Page 59

出力

PHP Warning: assert(): assert(3 === 1.5 + 1.5) failed in /Users/
megurine/repo/php/phperkaigi-test/assert-test.php on line 11

Warning: assert(): assert(3 === 1.5 + 1.5) failed in /Users/megurine/
repo/php/phperkaigi-test/assert-test.php on line 11
PHP Warning: assert(): assert('2112-09-03' === date('Y-m-d',
4503168000)) failed in /Users/megurine/repo/php/phperkaigi-test/assert-
test.php on line 12

Warning: assert(): assert('2112-09-03' === date('Y-m-d', 4503168000)) failed in /Users/megurine/repo/php/phperkaigi-test/assert-test.php on
line 12

Page 60

エラーハンドリングの

設定をしないと
出力が二重化される

Page 61

まあ雑だけど
テストにはなる

Page 62

Page 63

Page 64

最低限ができると
人間は欲が出てくる

Page 65

できる!
フレームワーク

Page 66

フレームワーク
とは

Page 67

https://ja.wikipedia.org/wiki/ソフトウェアフレームワーク

Page 68

制御の反転
ソフトウェアフレームワークの
特徴

Page 69

ふつうのライブラリは

あなたが書いたコードから
呼び出される

Page 70

フレームワークは
あなたが書いたコードを
呼び出す

Page 71

これが
制御の反転

Page 72

ここからは
どんどんいきます

Page 73

仕様1: コードをべたに実行

テストファイルは xxx_test.phpの形式

assert()を使ってテストする

スクリプトを実行すると実行

Page 74

assert_tests/run

#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php'; $testing = include __DIR__ . '/bootstrap.php';
chdir(__DIR__);
foreach (glob('*_test.php') as $file) {
try {
include $file;
} catch (\Throwable $e) {
dump($e);
}
}
$testing->finalize();

Page 75

assert()に失敗した

ときの結果は
カスタマイズできる

Page 76

(実はPHP5方式と
PHP7方式で異なる) 今回はPHP5方式

Page 77

bootstrap.php

<?php
require_once __DIR__ . '/define.php';
ini_set('zend.assertions', 1); ini_set('assert.exception', 0); assert_options(ASSERT_ACTIVE, 1); assert_options(ASSERT_BAIL, 0);
assert_options(ASSERT_QUIET_EVAL, 0);
assert_options(ASSERT_WARNING, 0);

$testing = new Testing();
assert_options(ASSERT_CALLBACK, [$testing, 'handler']);
return $testing;

Page 78

Testing.php
<?php
class Testing {
/** @var bool テスト失敗停止するか */

private $stop_on_failure = false;
/** @var array[] 実行結果 */

private $result = [];

public function __construct(array $options) {
$this->stop_on_failure = $options['stop_on_failure'] ?? false;
}

/** assert()の失敗時にこいつが呼ばれるように設定 */

public function handler($file, $line, $code, $desc = null) {
$this->result[] = [$file, $line, $desc];
if ($this->stop_on_failure) $this->finalize();
}

Page 79

Testing.php
/** いままで失敗したテストをまとめて結果表示 */

public function finalize(): void
{
if ($this->result) {
foreach ($this->result as [$file, $line, $desc])
{
$this->output($file, $line, $desc);
}
exit(1); // エラー終了

}
$this->result = []; echo 'ok.', PHP_EOL;
exit(0);
}

Page 80

Testing.php
private function output($file, $line, $desc): void
{
// ソースコードの該当行を取得

$code = trim(file($file)[$line-1]);

echo "FILE: {$file} ({$line})", PHP_EOL;
echo "CODE: {$code}", PHP_EOL;

echo "DESC: {$desc}", PHP_EOL;
}

Page 81

完成!

Page 82

テストを記述

Page 83

fizzbuzz_test.php
<?php
// to_fizzbuzz() 関数は別に定義済みの想定

assert("1" === $actual = to_fizzbuzz(1)); assert("Fizz" === $actual = to_fizzbuzz(3)); assert("Buzz" === $actual = to_fizzbuzz(5)); assert("Buzz" === $actual = to_fizzbuzz(10));
assert("FizzBuzz" === $actual = to_fizzbuzz(15));

Page 84

Page 85

完璧では
(ただしassertは一行で書かれるものとする)

Page 86

😊

Page 87

さらに
パワーアップ

Page 88

PowerAssert (のようなもの)

Page 89

Groovyのテストで
一躍知られた

Page 90

t-wadaさんの
power-assert.jsが有名

Page 91

http://hamcrest.org/

Page 92

PHPでちゃんと
作ろうとすると大変

Page 93

それっぽいもの
なら作れる!

Page 94

Page 95

欠点

Page 96

assert()では失敗しかフック できないから、「98/100成功」 のようなカウントができない

Page 97

PowerAssert(もどき)
書くのがめんどい

Page 98

Page 99

define.php
<?php

const collect_vars = 'return v(get_defined_vars()+
["this"=>isset($this)?$this:null]);';

function v(array $variables): string
{

// $GLOBALSは長大なので消しとく

unset($variables['GLOBALS']); return \serialize($variables);
}

Page 100

次のパターンに
行きましょう

Page 101

仕様2: 関数を実行

テストファイルは xxx_test.phpの形式

その中に test_xxxx() 関数が定義する

Hamcrestを使ってテストしてみる

スクリプトを実行すると実行

Page 102

http://hamcrest.org/

Page 103

Hamcrest (hamcrest-php)

マッチャー

(値がマッチするか比較)

もともとはJavaのライブラリ

assertXXX()みたいなのが定義されてる

\Hamcrest\Util::registerGlobalFunctions()

を呼ぶとグローバル関数として定義される

Page 104

assert_tests/run (1/3)

#!/usr/bin/env php
<?php
require __DIR__ . '/../vendor/autoload.php';

\Hamcrest\Util::registerGlobalFunctions();

chdir(__DIR__);
foreach (glob('*_test.php') as $file) {
require_once $file;
}

$failures = []; $errors = [];

Page 105

assert_tests/run (2/3)

foreach (get_defined_functions(true)['user'] as $func) {
if (strpos($func, 'test_') === 0) {
try {
$func(); echo '.';
} catch (\Hamcrest\AssertionError $e) {
$failures[] = $e;
echo 'F';
}catch (\Throwable $e) {
$errors[] = $e;
echo 'E';
}
}
}
echo PHP_EOL;

Page 106

assert_tests/run (3/3)

if ($failures) {
echo "Failures:", PHP_EOL;

foreach ($failures as $f) {
echo PHP_EOL;
echo "FILE: {$f->getFile()} ({$f->getLine()})", PHP_EOL;
echo "DESC: {$f->getMessage()})", PHP_EOL;
}
}

if ($errors) {
echo "Errors:", PHP_EOL;
dump($errors);
}

Page 107

また完成して
しまった…

Page 108

テストを記述

Page 109

fizzbuzz_test.php
<?php

function test_fizzbuzz()
{
assertThat(to_fizzbuzz(1), equalTo("1"));
assertThat(to_fizzbuzz(3), equalTo("Fizz")); assertThat(to_fizzbuzz(5), equalTo("Buzz")); assertThat(to_fizzbuzz(10), equalTo("Buzz"));
assertThat(to_fizzbuzz(15), equalTo("FizzBuzz"));
}

Page 110

Page 111

🤔

Page 112

出力差分はわかるけど いまいちわかりにくい

Page 113

そうだ

パラメタライズテスト
をしよう

Page 114

引数だけを変更して
同じテストをする

Page 115

PHPUnitでの

@dataProvider

Page 116

名前も拝借して
いきましょう

Page 117

fizzbuzz_test.php (Before)
<?php

function test_fizzbuzz()
{
assertThat(to_fizzbuzz(1), equalTo("1"));
assertThat(to_fizzbuzz(3), equalTo("Fizz")); assertThat(to_fizzbuzz(5), equalTo("Buzz")); assertThat(to_fizzbuzz(10), equalTo("Buzz"));
assertThat(to_fizzbuzz(15), equalTo("FizzBuzz"));
}

Page 118

fizzbuzz_test.php (After)

<?php /**
* @dataProvider fizzbuzz_number_provider
*/
function test_fizzbuzz2($expected, $input)
{
assertThat(to_fizzbuzz($input), equalTo($expected));
}

function fizzbuzz_number_provider()
{
return [
["1", 1], ["Fizz", 3], ["Buzz", 5], ["Buzz", 10], ["FizzBuzz", 15],
];
}

Page 119

実行側も
書き換えていきます

Page 120

assert_tests/run (2/3)

foreach (get_defined_functions(true)['user'] as $func) {
if (strpos($func, 'test_') === 0) {
try {
$func(); echo '.';
} catch (\Hamcrest\AssertionError $e) {
$failures[] = $e;
echo 'F';
}catch (\Throwable $e) {
$errors[] = $e;
echo 'E';
}
}
}
echo PHP_EOL;

Page 121

assert_tests/run After

foreach (get_defined_functions(true)['user'] as $func) {
if (strpos($func, 'test_') === 0) {
$provider = get_provider($func) ?: function (){ yield []; };

foreach ($provider() as $args) {
try {
$func(...$args);
echo '.';
} catch (\Hamcrest\AssertionError $e) {
$failures[] = ['error' => $e, 'args' => $args];
echo 'F';
}catch (\Throwable $e) {
$errors[] = $e;
echo 'E';
}

Page 122

assert_tests/run getProvider

function get_provider(string $func_name): ?string
{
$ref = new \ReflectionFunction($func_name);

// @dataProvider hogehoge を取り出す

if (preg_match('/@dataProvider +(?<provider>\S+)/',
$ref->getDocComment(), $m)) {
return trim($m['provider']);
}

return null;
}

Page 123

Page 124

まとめると

Page 125

単にテストを実行する

フレームワークっぽいもの
は意外と簡単に作れる

Page 126

PHPでテスト しにくい状態

Page 127

ここまでは「理想的な」

関数をテストすること
しか想定してない

Page 128

理想的

||
引数と返り値だけ

Page 129

PHPで書かれる

「現実」のコードは
それだけじゃない

Page 130

ファイル書き込み、データベース、
外部HTTP API呼び出し、グロー
バル変数、メール送信、HTTP
ヘッダ、クッキー…… などなど

Page 131

例: CLIでcookie()とか header()が呼び出され るとFatal errorになる

Page 132

これはテスティングフレーム ワークだけではなくアプリケー ションの構造も再設計しない
うまくテストできない

Page 133

HTTPはPSR-7に
するとか

Page 134

外部への依存を

抽象化して
注入するとか

Page 135

テスト実行中に

専用のDBを
起動して利用するとか

Page 136

テストしたいと思った

瞬間に直さなきゃ

いけないことが
爆発的に発生して氏ぬ

Page 137

😇

Page 138

次回に続く…?

Page 139

まじめにテスト やりたいひとへ

Page 140

おまけ

Page 141

Page 142

Page 143

PHPUnitも
良いが

Page 144

phpspecも
すごいぞ

Page 145

Page 146