Skip to content

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

公開日:

東京都練馬区練馬区立区民・産業プラザ Coconeriホールで開催された『PHPerKaigi 2018 前夜祭』でレギュラーセッション(30分)として発表しました。

Download PDF

スライドテキスト

Page 1

PHPでテスティングフレ…ryを

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

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 191890

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--Compute 1 + 1 test--FILE--<?= 1 + 1 ?>--EXPECT--2

--TEST--GET $_ENV test--ENV--return <<<END a=AAA b=BBB 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