テストコードが書けるようになって「変更したけど壊してないかな」という不安を解消しませんか?〜テスト駆動開発の世界のクイックツアーも添えて〜

テストコードが書けるようになって「変更したけど壊してないかな」という不安を解消しませんか?

〜テスト駆動開発の世界のクイックツアーも添えて〜

PHPカンファレンス関西2024 2/11 nikkie

はーいほーーー!!

㊗️6年ぶりの開催(スタッフの方々、ありがとうございます!!👏❤️)

関西のPHPerの皆さん、こんにちは

  • 心中お察しします。「「初手奇声!?!?」」

  • これ以上空気が凍ることはないはずです。 気づきのアウトプット、お気軽に #phpkansai

導入:「良いコードを書くための心得」のトークです

PHPカンファレンス関西 優遇テーマ

初心者PHPer向けのトーク

良いコードを書くための心得

PHPカンファレンス関西2024の採択方法を公開します

良いコードとは?(IMO)

  • 前提として、ユーザに 価値 を届けるコード

  • =良い 振る舞い のコード

私は良い 構造 のコードにもこだわりたい!

  • 変更しやすい コード(まさしく"ソフト"ウェア)

  • プログラミング言語の 機能 を引き出したコード(道具をうまく使う)

  • 動作するきれいなコード」(ケント・ベック)

大事なのは、より良く していく

  • つよつよになれば一発で良いコードが書けると思っていた。幻想なのかも

  • いまの方向性:より変更しやすく/より言語機能を使うように 実装を改善 していく

  • 不安「変更したけど壊してないかな」(今回の話)

申し遅れました。にっきーと申します(「お前、誰よ」)

  • nikkie / @ftnext @ftnext

  • ソフトウェア開発 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. テストコード入門

  2. テストの世界のクイックツアー

入門 して行ってください!

  1. テストコード入門

  • テストコードが初めての方が「書いてみよう」と思っていただけたら嬉しいです

  1. テストの世界のクイックツアー

先の世界も垣間見ましょう

  1. テストコード入門

  2. テストの世界のクイックツアー

  • 「今はまだわからないところもあるけれど、 こんな世界 があるのか〜」

1部:テストコード入門

  1. テストコードが書けるメリット

  2. 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)

(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簡単に確認 できる

actual

expected

fizzbuzz(3)

"Fizz"

fizzbuzz(5)

"Buzz"

fizzbuzz(15)

"FizzBuzz"

不安は退屈に変わる

  • 実装中、仕様を満たす 動作するコード であると確認できる🙌

  • 書き換える際も、おかしくしていたら気付ける 🙌(回帰テスト

  • ただし、このトークで扱うテストコードとは 開発者のためのテスト (QAのテストとは別)

書くコードは増えている、けれど

  • 実装に加えてテストコードも書く

  • でも、デメリット << メリット だと思うので、書いていく(書ける方を増やしたい!)

🥟テストコードは良いコードを書く力をつける下地(N=1)

  • テストコードを書くだけでは良いコードにはならない

  • テストコードは良いコードに近づけるのを助けてくれる

    • 新しく知った文法を試して書き換えるとき、 誤って振る舞いを変えてしまっても気づける

PHPUnitでテストを書こう

1部 2/2章:先のfizzbuzz関数のテストを書いてみましょう

PHPUnit

PHPUnitをインストール

  • Composer

  • PHAR

どちらかをやればいいです

Composerでインストール

  • composer require --dev phpunit/phpunit

  • ちょうぜつ本 など

.
├── composer.json
├── composer.lock
└── vendor/
    └── bin/
        └── phpunit

PHARでインストール

.
└── tools/
    └── phpunit

tips: Composerではautoload 🏃‍♂️ (skip)

require_once をカッコよく書けます

composer.json 参考例
{
    "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 ステップ

  1. テストコードのファイルを作る

  2. クラスを書く

  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)

🥟PHPUnitで書くテストは3ステップ

  1. テストコードのファイルを作る(*Test.php

  2. クラスを書く(*Test

  3. テストケースとしてメソッドを書く(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 検証

https://xp123.com/articles/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)

🥟テストコードの構成要素: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

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部構成

  1. テストコード入門

  2. テストの世界のクイックツアー

閑話休題🍵 PHP触ってみて💭

  • 文末の セミコロン 忘れがち

  • 実行時に型を保証 、いいな〜(Pythonの型は添えるだけ。実行時は無視)

  • デファクトスタンダードのテストライブラリ!(Pythonには複数候補があります。参考『Python実践レシピ』)

2部:テストの世界のクイックツアー

  1. モック

  2. テスト駆動開発

発展的話題:「モック」

2部 1/2章

いまは分からなかったとしても大丈夫

繰り返し挑んで、 少しずつ理解 していけばいいんです

別の処理を呼び出す実装のテストコード

  • 全ての処理を通したテストも書ける

  • 別の処理が HTTP通信 のような場合どうするか?

  • -> モック (後述する 広義

モック(mock)

  • 偽物、模造品(ウィズダム英和辞典)

  • 画面の"モック"アップ

  • テストコードの文脈では、 広義狭義 がある(『xUnit Test Patterns』)

広義のモック

  • Test Doubles を指して「モック」

  • テスト用の 代役

  • テスト対象の関数やメソッドの実装の中で依存するものの代役

参考:『xUnit Test Patterns』における整理

具体的な代役にはいろいろある

../_images/xutp-test-doubles.gif

http://xunitpatterns.com/Types%20Of%20Test%20Doubles.gif

スタブ(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つずつ追加し、仮実装から一般化した場合、テストケースとして片方消せる

これの何がいいの?

  • 初見の私「茶番では?」

  • 完全にコントロール している状態

  • テストも実装も間違えていない。不安は退屈に変わっている

もっと知りたい方へ

小さい テスト駆動開発

私はこのスタイルに行き着きつつあります

小さいは、正義!

  • お見せしたテスト駆動開発は、テストコードをバーっと書き上げた。その後実装をバーっと書く

  • もっと 小さいステップ でできます(参考:『Clean Craftsmanship』第2章)

Uncle Bob流

TODOの1つの単位で、Red -> Green を 何度も行き来 する

  • require できないからRed🟥。ファイルを置いてGreen🟩

  • fizzbuzz 関数を呼び出すけれどそもそもないからRed🟥。用意だけしてGreen🟩

デモをする(左右に開いて)

  • 環境構築の確認

  • テストを書く(requireできない)

  • 関数を呼び出せないを解決

  • 該当するメソッド

Python版:https://ftnext.github.io/small-technical-2023/

テストコードが書けるようになって「変更したけど壊してないかな」という不安を解消しませんか?

  • 主張:好きとか嫌いとかはいい、 テストコードを書く んだ

  • 不安「変更したけど壊してないかな」は、退屈「テストが通っているから大丈夫」に変わります

ご清聴ありがとうございました。おおきに!

今できないことがあっても大丈夫。これからできるようになればいい

アニメ ミリオンライブ! 第9話より

参考資料

参考書籍

PyConで話した内容のPHP版でした

PHPカンファレンスのアーカイブより

t-wadaさん関連🦁

テスト駆動開発のはじめ方参考🔰

自分のエントリより ちょうぜつ本読書ログ

自分のエントリより 登壇準備中のアウトプット

自分のエントリより PHP環境構築

EOF