テストコードが書けるようになって「変更したけど壊してないかな」という不安を解消しませんか?〜テスト駆動開発の世界のクイックツアーも添えて〜
テストコードが書けるようになって「変更したけど壊してないかな」という不安を解消しませんか?
〜テスト駆動開発の世界のクイックツアーも添えて〜
PHPカンファレンス関西2024 2/11 nikkie
はーいほーーー!!
㊗️6年ぶりの開催(スタッフの方々、ありがとうございます!!👏❤️)
関西のPHPerの皆さん、こんにちは
心中お察しします。「「初手奇声!?!?」」
これ以上空気が凍ることはないはずです。 気づきのアウトプット、お気軽に
#phpkansai
導入:「良いコードを書くための心得」のトークです
PHPカンファレンス関西 優遇テーマ
初心者PHPer向けのトーク
良いコードを書くための心得
良いコードとは?(IMO)
前提として、ユーザに 価値 を届けるコード
=良い 振る舞い のコード
私は良い 構造 のコードにもこだわりたい!
変更しやすい コード(まさしく"ソフト"ウェア)
プログラミング言語の 機能 を引き出したコード(道具をうまく使う)
「動作するきれいなコード」(ケント・ベック)
大事なのは、より良く していく
つよつよになれば一発で良いコードが書けると思っていた。幻想なのかも
いまの方向性:より変更しやすく/より言語機能を使うように 実装を改善 していく
不安「変更したけど壊してないかな」(今回の話)
申し遅れました。にっきーと申します(「お前、誰よ」)
ソフトウェア開発 2016年〜(歴8年)
2019年〜 株式会社ユーザベースのデータサイエンティスト。 Python ・機械学習(We're hiring!)
変更しやすいコードに情熱を持ったPython使いです
プライベート:1日1エントリ https://nikkie-ftnext.hatenablog.com/
テストコードは業務や趣味でPythonで書いて 4年半 程度🐍
PHPのコードにもっといい書き方あったらぜひ教えてください🙏🔰
テストコード についての発表です
今日初めてテストコードに入門するという方?🙋♂️
テストコードは書いたことがなくて全然OK👌
プログラミング言語を超えた 考え方 をお伝えできればと思います
聴きに来てくださってありがとうございます
今できないことがあっても大丈夫。これからできるようになればいい
アニメ ミリオンライブ! 第9話より(感想ブログ)
動作環境
PHP 8.3.2
PHPUnit
PHAR: 10.5.10 / 11.0.2
Composer: 10.5.10
お品書き:2部構成
テストコード入門
テストの世界のクイックツアー
入門 して行ってください!
テストコード入門
テストコードが初めての方が「書いてみよう」と思っていただけたら嬉しいです
テストの世界のクイックツアー
先の世界も垣間見ましょう
テストコード入門
テストの世界のクイックツアー
「今はまだわからないところもあるけれど、 こんな世界 があるのか〜」
1部:テストコード入門
テストコードが書けるメリット
PHPUnitでテストの書き方
テストコードが書けるメリット
1部 1/2章
壊してないかなという 不安 に向き合う
良いコード(変更しやすい、言語の機能を引き出している)に近づけていく
例:全霊をかけたコードだが、よりよい書き方を知って書き換える
変更するたびに、 壊していないか 不安
例:FizzBuzz
<?php
function fizzbuzz($number)
{
if ($number % 3 == 0 and $number % 5 == 0) {
return "FizzBuzz";
} elseif ($number % 3 == 0) {
return "Fizz";
} elseif ($number % 5 == 0) {
return "Buzz";
} else {
return "$number";
}
}
// echo fizzbuzz(3) . "\n";
// echo fizzbuzz("5") . "\n";
型を書く(PHPの機能をもっと使う)
<?php
declare(strict_types=1);
function fizzbuzz(int $number): string
{
if ($number % 3 == 0 and $number % 5 == 0) {
return "FizzBuzz";
} elseif ($number % 3 == 0) {
return "Fizz";
} elseif ($number % 5 == 0) {
return "Buzz";
} else {
return "$number";
}
}
// echo fizzbuzz(3) . "\n";
// echo fizzbuzz("5") . "\n"; // TypeError
書き換えで振る舞いを変えていないだろうか?
不安 に対処する3つのアプローチ
(A) 祈る 🙏
🙏🙏「どうか変わっていませんように」🙏🙏
振る舞いを変えていないか不安だが、 何も確認はしない
(B) 手で動作確認 ✋
例えば対話的に
fizzbuzz
関数を実行(php -a)
php > require 'fizzbuzz.php';
php > echo fizzbuzz(15);
FizzBuzz
安心できるが、 関数の数が増えて いくと現実的ではなさそう
参考:PHPの対話シェル 🏃♂️ (skip)
https://www.php.net/manual/ja/features.commandline.interactive.php
はじめてのPHPコマンドラインオプション (ことみんさん。PHPカンファレンス沖縄2023)で知りました
(C) コードを書いて動作確認 🤖
この発表の本題
「手で動作確認」の 自動化 (テストコードを書く)
プログラムで使う部品のコードは、プログラムを書いて動作確認するという考え方(ref: ちょうぜつ本)
テストコードの世界へようこそ🎉
初見だと独特と感じる 用語 を紹介
テストケース
1つ1つのテストのこと
テストコードがある=複数のテストケースがある
実行結果
テストケースをすべて実行すると
pass (全て通る・成功)
fail (1つでも失敗・落ちる)
テストにおける 値 の呼び方
テスト対象(の関数やメソッド)を実行した値: actual value
期待結果: expected value
呼び方を反映した変数名
php > $actual = fizzbuzz(15); // fizzbuzz関数がテスト対象
php > $expected = "FizzBuzz";
php > var_dump($actual === $expected);
bool(true)
テストコードがあると
actual |
expected |
|
|
|
|
|
|
不安は退屈に変わる
実装中、仕様を満たす 動作するコード であると確認できる🙌
書き換える際も、おかしくしていたら気付ける 🙌(回帰テスト)
ただし、このトークで扱うテストコードとは 開発者のためのテスト (QAのテストとは別)
書くコードは増えている、けれど
実装に加えてテストコードも書く
でも、デメリット << メリット だと思うので、書いていく(書ける方を増やしたい!)
🥟テストコードは良いコードを書く力をつける下地(N=1)
テストコードを書くだけでは良いコードにはならない
テストコードは良いコードに近づけるのを助けてくれる
新しく知った文法を試して書き換えるとき、 誤って振る舞いを変えてしまっても気づける
PHPUnitでテストを書こう
1部 2/2章:先のfizzbuzz関数のテストを書いてみましょう
PHPUnit
PHPでテストコードを書くためのライブラリ
PHPUnitをインストール
Composer
PHAR
どちらかをやればいいです
Composerでインストール
composer require --dev phpunit/phpunit
ちょうぜつ本 など
.
├── composer.json
├── composer.lock
└── vendor/
└── bin/
└── phpunit
PHARでインストール
PHPUnitのドキュメントで推している
https://docs.phpunit.de/en/10.5/installation.html#manual-download-of-phar
.
└── tools/
└── phpunit
tips: Composerではautoload 🏃♂️ (skip)
require_once
をカッコよく書けます
{
"autoload": {
"psr-4": {"FizzBuzz\\": "src/"}
},
"autoload-dev": {
"psr-4": {"FizzBuzz\\": "tests/"}
}
}
phpunitコマンドの使い方
vendor/bin/phpunit
(PHARを配置した場合 tools/phpunit)
Usage:
phpunit [options] <directory|file> ...
PHPUnitで書くテストは 3 ステップ
テストコードのファイルを作る
クラスを書く
テストケースとしてメソッドを書く
Step1 ファイル作成
https://docs.phpunit.de/en/10.5/installation.html#manual-download-of-phar
.
├── src/
│ └── fizzbuzz.php
├── tests/
│ └── fizzbuzzTest.php
└── vendor/
(Composerでインストールした場合で進めます。PHARでも動作確認しています)
テストを書くファイル
tests
ディレクトリに配置ファイル名は *Test.php
phpunit
コマンドにディレクトリのパスが指定されたときに探されるファイル
Step2 クラスを書く
PHPUnitが提供する TestCaseクラスを継承 したクラスを書く
クラス名は *Test
use PHPUnit\Framework\TestCase;
class FizzbuzzTest extends TestCase
{
}
Step3 テストケースとしてメソッドを書く (1/2)
testで始まる メソッド名
class FizzbuzzTest extends TestCase
{
public function test_3の倍数のときはFizzを返す(): void
{
}
}
Step3 テストケースとしてメソッドを書く (2/2)
fizzbuzz(3)
と呼び出した返り値は、文字列 "Fizz"
と型と値が同じ(と表明)
class FizzbuzzTest extends TestCase
{
public function test_3の倍数のときはFizzを返す(): void
{
$this->assertSame(fizzbuzz(3), "Fizz");
}
}
今回はassertSameが適切。assertEqualsではない
php > var_dump("1" === "1"); // assertSame
bool(true)
php > var_dump("1" == 1); // assertEquals
bool(true)
php > var_dump("1" === 1); // assertSameなら誤りに気づける
bool(false)
Test
Attributeでもよい 🏃♂️ (skip)
https://docs.phpunit.de/en/10.5/attributes.html#appendixes-attributes-test
use PHPUnit\Framework\Attributes\Test;
class FizzbuzzTest extends TestCase
{
// 関数名は3で始められないので、_を付けています(他のやり方もあると思います)
#[Test]
public function _3の倍数のときはFizzを返す(): void
{
}
}
日本語テストメソッド 🏃♂️ (skip)
IMO:関数名は 日本語を使ってもよい と思います
🥟PHPUnitで書くテストは3ステップ
テストコードのファイルを作る(
*Test.php
)クラスを書く(
*Test
)テストケースとしてメソッドを書く(
test*
)
https://docs.phpunit.de/en/10.5/writing-tests-for-phpunit.html#asserting-return-values
テスト実行
vendor/bin/phpunit tests
(PHARを配置した場合 tools/phpunit tests)
passしたとき
Runtime: PHP 8.3.2
. 1 / 1 (100%)
Time: 00:00, Memory: 22.77 MB
OK (1 test, 1 assertion)
failしたとき(わざと落とした)
Runtime: PHP 8.3.2
F 1 / 1 (100%)
Time: 00:00.001, Memory: 22.77 MB
There was 1 failure:
1) fizzbuzz\FizzbuzzTest::test_3の倍数のときはFizzを返す
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'3'
+'Fizz'
/.../tests/fizzbuzzTest.php:16
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
テストコードの構成要素
$this->assertSame(fizzbuzz(3), "Fizz");
関数に ある値を入力したときの出力 を検証した
3A という見方を導入
3A
Arrange 準備
Act 実行
Assert 検証
3Aで見るPHPUnit
1ステップ1行になるように書き直しています
class FizzbuzzTest extends TestCase
{
public function test_3の倍数のときはFizzを返す(): void
{
$number = 3;
$actual = fizzbuzz($number);
$expected = "Fizz";
$this->assertSame($actual, $expected);
}
Arrange
テストの 準備 (データの用意など)
class FizzbuzzTest extends TestCase
{
public function test_3の倍数のときはFizzを返す(): void
{
$number = 3;
$actual = fizzbuzz($number);
$expected = "Fizz";
$this->assertSame($actual, $expected);
}
Act
テスト対象を 実行 (呼び出す)
class FizzbuzzTest extends TestCase
{
public function test_3の倍数のときはFizzを返す(): void
{
$number = 3;
$actual = fizzbuzz($number);
$expected = "Fizz";
$this->assertSame($actual, $expected);
}
Assert
実行結果が期待値と等しいかを 検証
class FizzbuzzTest extends TestCase
{
public function test_3の倍数のときはFizzを返す(): void
{
$number = 3;
$actual = fizzbuzz($number);
$expected = "Fizz";
$this->assertSame($actual, $expected);
}
第4のA:Annihilate 🏃♂️ (skip)
クリーンアップ
『ロバストPython』第21章より
🥟テストコードの構成要素:3A
Arrange テストの 準備
Act テスト対象の 実行
Assert 実行結果と期待値の 検証
tips✨ パラメタ化 テスト
public function test_3の倍数のときはFizzを返す(): void
{
}
3の倍数ならFizz
$number
の 取りうる値は複数
3
6
9
個別にテストの関数を書く?
public function test_3の倍数のときはFizzを返す_3の場合(): void
{
}
public function test_3の倍数のときはFizzを返す_6の場合(): void
{
}
テストメソッドに引数 (パラメタ)を渡せたら!
class FizzbuzzTest extends TestCase
{
// $numberに3や6を渡せたら
public function test_3の倍数のときはFizzを返す($number): void
{
$this->assertSame(fizzbuzz($number), "Fizz");
}
}
PHPUnitでは Data Provider でできます!
use PHPUnit\Framework\Attributes\DataProvider;
class FizzBuzzParametrizedTest extends TestCase
{
public static function provide_3の倍数(): array
{
return [[3], [6]];
}
#[DataProvider("provide_3の倍数")]
public function test_3の倍数のときはFizzを返す(int $number): void
{
$this->assertSame(fizzbuzz($number), "Fizz");
}
}
Data Provider
https://docs.phpunit.de/en/10.5/writing-tests-for-phpunit.html#data-providers
A data provider method must be public and static.It must return a value that is iterable, either an array or an object that implements the Traversable interface.
Data Providerの例
public static function provide_3の倍数(): array
{
return [[3], [6]];
}
#[DataProvider("provide_3の倍数")]
public function test_3の倍数のときはFizzを返す(int $number): void
$number
に、3、6と順次渡るドキュメントには1回あたり複数の変数を渡す例あり
1つの関数、複数のテストケース
個別に書いたのと同じ 結果が得られる
% tools/phpunit tests/parametrizedTest.php
PHPUnit 10.5.10 by Sebastian Bergmann and contributors.
Runtime: PHP 8.3.2
.. 2 / 2 (100%)
Time: 00:00.001, Memory: 22.77 MB
OK (2 tests, 2 assertions)
🌯1部「テストコード入門」まとめ
テストは 良いコードに近づけていく助け になります
PHPUnitでテストを書く方法 を紹介(3A)
Data Providerによるパラメタ化テスト
お品書き:2部構成
テストコード入門
テストの世界のクイックツアー
閑話休題🍵 PHP触ってみて💭
文末の セミコロン 忘れがち
実行時に型を保証 、いいな〜(Pythonの型は添えるだけ。実行時は無視)
デファクトスタンダードのテストライブラリ!(Pythonには複数候補があります。参考『Python実践レシピ』)
2部:テストの世界のクイックツアー
モック
テスト駆動開発
発展的話題:「モック」
2部 1/2章
いまは分からなかったとしても大丈夫
繰り返し挑んで、 少しずつ理解 していけばいいんです
別の処理を呼び出す実装のテストコード
全ての処理を通したテストも書ける
別の処理が HTTP通信 のような場合どうするか?
-> モック (後述する 広義)
モック(mock)
偽物、模造品(ウィズダム英和辞典)
画面の"モック"アップ
テストコードの文脈では、 広義 と 狭義 がある(『xUnit Test Patterns』)
広義のモック
Test Doubles を指して「モック」
テスト用の 代役
テスト対象の関数やメソッドの実装の中で依存するものの代役
参考:『xUnit Test Patterns』における整理
具体的な代役にはいろいろある
スタブ(Stub)
テスト対象から呼び出す処理が 実際に動かないように スタブにする
例:HTTP通信処理をスタブにする
サービスが落ちていなくても異常系のレスポンスが返った(テスト対象に入力された)ことにできる
Mock Object
狭義のモック
スタブのように、テスト対象から呼び出す処理を実際に動かないようにできる
加えて、テスト対象が モックを期待通り使っているかをassert できる
例:HTTP通信を呼び出すコードにテストを書きたい
function foo(HttpCommunication $communication): void
{
echo "foo start\n";
$status_code = $communication->communicate();
echo "foo end\n";
}
例:HTTP通信
class HttpCommunication
{
public function communicate(): int
{
$status_code = 200; // ダミーの値
echo "HTTP通信をしています...\n";
return $status_code;
}
}
モックにするためにクラスで実装
Test Doublesを使って書いたテストコード
public function test_モックを使うテストの例(): void
{
$mocked_communication = $this->createMock(HttpCommunication::class);
$mocked_communication->expects($this->once())
->method("communicate")
->with()
->willReturn(200);
foo($mocked_communication);
}
こんなときにTest Doublesを使っています
外部と通信する関数(通信エラーでテストが落ちうる)
時間のかかる関数(テストの実行時間が伸びる)
出力が変わる関数(例:random)
🥟モック(Test Doubles)
テストにおける 代役 (具体はStubやMock Object)
例えば、外部と通信する処理はモックする
モックをもっと知りたい方へ 🏃♂️ (skip)
テスト駆動開発
2部 2/2章:テストコードが書けるようになると開く扉の中の世界のご紹介
TDD: Test Driven Development
開発手法の1つ。テストが開発を駆動(運転)
考案者 ケント・ベック(『テスト駆動開発』)
開発者のためのテスト を使って、開発を進めていく(再掲:QAのテストとは別)
ケント・ベックの定義
自動化されたテストが失敗したときのみ、新しいコードを書く。
重複を除去する。
2つのルール 『テスト駆動開発』Kindle の位置No.33-34
TDDはサイクル♻️
Red -> Green -> Refactor
何度も何度も 回す
Red🟥
まずテストコードを書く
テストファーストと呼ばれる
実装はないので、もちろんテストは落ちる(fail)
Green🟩
実装する(後述)
テストが通るようになる(pass)
テストを通すのを最優先 (「動作する」コード)
Refactor
テストが通っている状態でリファクタリングを実施して「 動作するきれいなコード 」にする
例:重複を除去する
実装だけでなくテストコードもリファクタリング対象
TDDのイメージ
何もないところからfizzbuzz関数を実装します
TODOリスト
- [ ] 数をそのまま文字列に変換する
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
必要になりそうなテストのリスト
数をそのまま文字列に変換する って何だ?
- [ ] 数をそのまま文字列に変換する
- [ ] 1を渡すと文字列1を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
1を渡すと文字列1を返す テスト 🟥
class FizzbuzzTest extends TestCase
{
public function test_1を渡すと文字列1を返す(): void
{
$this->assertSame(fizzbuzz(1), "1");
}
}
1を渡すと文字列1を返す 実装 🟩
function fizzbuzz(int $number): string
{
return "1";
}
仮実装 と呼ばれる
作ったテストを間違えていない確認
リファクタリング?
重複があるか? ーなし
Red -> Green -> Refactorの1サイクル終了♻️
実装を一般化したい! TODO追加
- [ ] 数をそのまま文字列に変換する
- [x] 1を渡すと文字列1を返す
- [ ] 2を渡すと文字列2を返す
- [ ] 3の倍数のときは数の代わりに「Fizz」に変換する
- [ ] 5の倍数のときは数の代わりに「Buzz」に変換する
- [ ] 15の倍数のときは数の代わりに「FizzBuzz」に変換する
2を渡すと文字列2を返す テスト 🟥
class FizzbuzzTest extends TestCase
{
public function test_2を渡すと文字列2を返す(): void
{
$this->assertSame(fizzbuzz(2), "2");
}
}
2を渡すと文字列2を返す 実装 🟩
function fizzbuzz(int $number): string
{
return "$number";
}
仮実装から一般化された実装へ(「三角測量」)
リファクタリング?
重複があるか?
Data Providerを検討できる
発展:具体例を1つずつ追加し、仮実装から一般化した場合、テストケースとして片方消せる
これの何がいいの?
初見の私「茶番では?」
完全にコントロール している状態
テストも実装も間違えていない。不安は退屈に変わっている
もっと知りたい方へ
t-wadaさんのライブコーディング をどうぞ
リズム がつかめるのでオススメ(視聴ログ(拙ブログ))
小さい テスト駆動開発
私はこのスタイルに行き着きつつあります
小さいは、正義!
お見せしたテスト駆動開発は、テストコードをバーっと書き上げた。その後実装をバーっと書く
もっと 小さいステップ でできます(参考:『Clean Craftsmanship』第2章)
Uncle Bob流
TODOの1つの単位で、Red -> Green を 何度も行き来 する
require
できないからRed🟥。ファイルを置いてGreen🟩fizzbuzz
関数を呼び出すけれどそもそもないからRed🟥。用意だけしてGreen🟩
デモをする(左右に開いて)
環境構築の確認
テストを書く(requireできない)
関数を呼び出せないを解決
該当するメソッド
テストコードが書けるようになって「変更したけど壊してないかな」という不安を解消しませんか?
主張:好きとか嫌いとかはいい、 テストコードを書く んだ
不安「変更したけど壊してないかな」は、退屈「テストが通っているから大丈夫」に変わります
ご清聴ありがとうございました。おおきに!
今できないことがあっても大丈夫。これからできるようになればいい
アニメ ミリオンライブ! 第9話より
参考資料
参考書籍
『ちょうぜつソフトウェア設計入門』(第6章)
『テスト駆動開発』(Kent Beck)
テストは不安を退屈に変える賢者の石だ。(第25章)
PyConで話した内容のPHP版でした
PHPカンファレンスのアーカイブより
PHPカンファレンス沖縄2023「素朴で考慮漏れのある PHP コードをテストコードとともに補強していく」
PHPカンファレンス2022 実践!ユニットテスト入門
PHPerKaigi 2019 「質」の良いユニットテストを書くためのプラクティス