はじめに
Namespaces are one honking great idea1、nikkieです。
2/11のPHPカンファレンス2024登壇2準備より1ネタ。
autoloadの設定でハマりました。
※この記事はハマったこととこうやったら解決できたというログを書き残すことを目的としています。
この方法がPHP界隈でいう"正解"なのかは自信がないところです(普段はPythonでTDDしていますが、PHPでテストを書くのが今回初めてです)。
PHPに関しては小さな点でもお気づきの点がありましたら、@ftnextまでお知らせください!
目次
- はじめに
- 目次
- PHPUnitでFizzBuzzのテストを書く
- ちょうぜつ本 6-4のコラムに従ってautoloadを導入
- 読み飛ばしたnamespaceの宣言
- うまくいく理由を数掘りだけしたログ
- 終わりに
PHPUnitでFizzBuzzのテストを書く
スタート地点はこちらです
nikkie「よーし、PHPUnitで小さなテスト駆動開発3をやってみるぞー」
. ├── src/ │ └── FizzBuzz.php ├── tests/ │ └── FizzBuzzTest.php ├── vendor/ ├── composer.json └── composer.lock
<?php class FizzBuzz { }
tests/FizzBuzzTest.php
<?php use PHPUnit\Framework\TestCase; require_once __DIR__ . "/../src/FizzBuzz.php"; class FizzBuzzTest extends TestCase { public function test_1を渡すと文字列1を返す(): void { $fizzBuzz = new FizzBuzz(); } }
これは通ります。
ただし「This test did not perform any assertions」とrisky表示です。
(小さいテスト駆動開発としてはこの後アサーションを追加するので、ここまではうまくいっています)
ここで思った(思ってしまった)のです。
nikkie「require_once __DIR__ . "/../src/FizzBuzz.php";
がちょっとかっこ悪いな。参考にしているちょうぜつ本にここの書き換えを扱ったコラムがあったぞ!」
ちょうぜつ本 6-4のコラムに従ってautoloadを導入
Composerのクラスローダーを使って、ファイル読み込みを書かなくてもクラス定義ファイルが読み込まれるようにするのが普通です。(Kindle版 pp.228-229)
nikkie「これだけでできるんだー。簡単そうだしやってみよう」
composer.jsonを編集します(リスト6-12 参考)4
{ "require-dev": { "phpunit/phpunit": "^10.5" + }, + "autoload": { + "psr-4": {"FizzBuzz\\": "src/"} + }, + "autoload-dev": { + "psr-4": {"FizzBuzz\\": "tests/"} } }
この状態でcomposer install
を叩きます。
% composer install Installing dependencies from lock file (including require-dev) Verifying lock file contents can be installed on current platform. Nothing to install, update or remove Generating autoload files 23 packages you are using are looking for funding. Use the `composer fund` command to find out more!
tests/FizzBuzzTest.phpを書き変えました
- require_once __DIR__ . "/../src/FizzBuzz.php"; + require_once __DIR__ . "/../vendor/autoload.php";
vendor/bin/phpunit tests
!(nikkie「動くでしょ」)
% vendor/bin/phpunit tests PHPUnit 10.5.7 by Sebastian Bergmann and contributors. Runtime: PHP 8.3.1 E 1 / 1 (100%) Time: 00:00.008, Memory: 6.00 MB There was 1 error: 1) FizzBuzzTest::test_1を渡すと文字列1を返す Error: Class "FizzBuzz" not found /.../tests/FizzBuzzTest.php:10 ERRORS! Tests: 1, Assertions: 0, Errors: 1.
nikkie「あれ?」
読み飛ばしたnamespaceの宣言
この解決にハマったのですが、結論から言うと、ちょうぜつ本を写経しきれていませんでした。
リスト6-13や6-14のnamespaceの宣言を読み飛ばしていました。
namespaceの宣言があるとうまく動きます
<?php + namespace FizzBuzz; class FizzBuzz { }
tests/FizzBuzzTest.php
<?php + namespace FizzBuzz; + use PHPUnit\Framework\TestCase; require_once __DIR__ . "/../vendor/autoload.php"; class FizzBuzzTest extends TestCase { public function test_1を渡すと文字列1を返す(): void { $fizzBuzz = new FizzBuzz(); } }
テストは流れ、require_onceを書き換える前と同じ結果です。
% vendor/bin/phpunit tests PHPUnit 10.5.7 by Sebastian Bergmann and contributors. Runtime: PHP 8.3.1 R 1 / 1 (100%) Time: 00:00.004, Memory: 6.00 MB There was 1 risky test: 1) FizzBuzz\FizzBuzzTest::test_1を渡すと文字列1を返す This test did not perform any assertions /.../tests/FizzBuzzTest.php:10 OK, but there were issues! Tests: 1, Assertions: 0, Risky: 1.
うまくいく理由を数掘りだけしたログ
composer install
vendor/composer/autoload_psr4.php を書き換えているんですね。
return array(
// 省略
'FizzBuzz\\' => array($baseDir . '/src', $baseDir . '/tests'),
// 省略
);
Composerのドキュメントを当たったところ
https://getcomposer.org/doc/01-basic-usage.md#autoloading
composer dump-autoload
を叩くとあり、「composer install
じゃない!?」と思いましたが、上記のようにinstallでも動いたのでこのコマンドの差はなさそうです(installはdump-autoloadを実行しているのかな)
上記のファイルが変更されていると知ったのはこちらの記事のおかげ
(この記事ではdumpautoload
コマンドです。ハイフンはあってもなくてもいいみたいですね)
namespaceの宣言
PHPのドキュメントを参照します。
PHP の名前空間は、関連するクラスやインターフェイス、関数、そして定数をひとまとめにして扱うものです。(名前空間の概要より)
<?php
の直後にnamespace
を書く必要があることを知ります。
名前空間を宣言するには、キーワード namespace を使用します。名前空間を含むファイルでは、他のコードより前にファイルの先頭で名前空間を宣言しなければなりません。(名前空間の定義より)
「2つのファイルにnamespace FizzBuzz;
(同じnamespace)って書いていいの?」という引っかかりには以下の説明を見つけました5。
(略) 同一の名前空間を複数のファイルで定義することができます。 これにより、ひとつの名前空間の内容をファイルシステム上で分割することができます。(名前空間の定義より)
FAQの一般的な質問も見ていきます。
https://www.php.net/manual/ja/language.namespaces.faq.php
じわじわ分かってきているのは、namespace FizzBuzz;
をsrcにもtestsにも書くと、tests側でFizzBuzz
と書いたときに「FizzBuzz名前空間のFizzBuzzクラス」のことを言っていそうだということ。
根拠はFAQ内の例7より
「修飾されていない name のようなクラス名はどのように解決される?」
それ以外(※補注 エイリアスを指定する import 文がない)の場合は、現在の名前空間が name の先頭に付け加えられます。
だから見つかるんですね。
名前空間を宣言していないとき、testsでFizzBuzzと書いたら名前空間の外部にあるグローバルクラスを意味しており(例2 参照)、そのためにnot found(見つからなかった)のかなというのが今の理解です
useを使ってもうまくいく
useなるキーワードもよく分かっていないのですが、tests側でuseしてもnot foundは解決しました。
(GPT-4に泣きついて知りました。not foundが解決した状態になり心の安寧を取り戻せました)
<?php - namespace FizzBuzz; - use PHPUnit\Framework\TestCase; + use FizzBuzz\FizzBuzz; require_once __DIR__ . "/../vendor/autoload.php"; class FizzBuzzTest extends TestCase { public function test_1を渡すと文字列1を返す(): void { $fizzBuzz = new FizzBuzz(); } }
分かっていないこと:
srcにもtestsにも同じnamespaceを宣言するのと、testsはuseにするの、優劣ってあるんだろうか?6
読み違えてるかもしれませんが、ちょうぜつ本は6-4のコラムのcomposer.jsonから前者を採用していると思われます
終わりに
PHPUnitを使う中でrequire_onceをかっこよくしようとComposerのautoloadに手を出し、ハマってから解決するまででした。
namespaceの宣言が肝でした!
-
The Zen of Pythonより。
python -m this
で確認できることを最近知りました↩ - 大阪でお会いしましょう! チケット:https://peatix.com/event/3752841/view↩
- Pythonの例でアウトプットしています。 PHP版は当日をお楽しみに!↩
-
キーの
"psr-4"
とはこちら ↩ - どちらもちょっとかじっただけの言語なので間違っていそうですが、PHPのnamespaceってJavaのpackageに近いのかも↩
- 親しんでいるPythonは、いろいろなやり方があるよりやり方は1つだけを好む文化なのです(The Zen of Pythonより「There should be one-- and preferably only one --obvious way to do it.」)↩