休日個人開発で学ぶテストコード! 画像に“集中線”を合成するツールを作ってみよう

プライベートでも何か作りたい! そんなときの「今日からはじめる休日個人開発」シリーズ、第二弾はテストコードを書きながら簡単なMVCモデルの画像加工ツールを作ってみましょう。好きな写真に集中線を合成できます。

休日個人開発で学ぶテストコード! 画像に“集中線”を合成するツールを作ってみよう

皆さん、プライベートで何か開発していますか? 「何か作りたい」という気持ちはあるものの、いまひとつ何から始めたらいいのか分からず、動けないままの人も多いと思います。

そんな皆さんのために、「仕事以外にも休日に個人で気軽に何かを作ってみよう!」という企画の第二弾です。今回は、第一弾で用意した開発環境を使って、画像を加工するツールを実際に作っていきます。

せっかくですので、ただ作るだけではなく、テストコードも一緒に書いてみましょう。最近は、CI(継続的インテグレーション)やCD(継続的デリバリー)も一般的になり、テストコードを書く機会が増えています。それを踏まえて、今回はテストコードを書く意義や、実際の書き方に焦点を当てていきます。

なぜテストコードを書くとよいのか

テストコードを駆使した開発手法に、TDDテスト駆動開発, test-driven development)があります。まず、TDDとテストコードの概要に触れておきます。

TDD(テスト駆動開発)とテストコード

TDDとは、簡単に言えばテストコードを中心とした開発手法のことです。TDDではプロダクトに新しい機能を追加する際に、先行してテストコードを書いたあと、それに対応するロジックを書いていきます。

TDDのサイクル

  1. 【Red】テストコードを書く
  2. 【Green】テストコードが動く最低レベルのソースコードを書く
  3. 【Refactor】テストコードが動く状態でそのソースコードをブラッシュアップさせる

最初はロジック自体が存在しないのでテストはもちろん失敗します(【Red】)。後から動作するロジックを書いていき(【Green】)、そのテストを動く状態に保ちながらリファクタリングしていく(【Refactor】)やり方です。

しかし、いきなりTDDを完璧にやろうとしても、失敗する可能性が高いです。始めのうちは雑でもいいので、テストコードを書くこと自体への抵抗をなくし、習慣化することが大切です。まずは、TDDまでやらなくとも、テストコードを書いてみましょう。

テストコードを書く意味

そもそも何のためにテストコードを書くのでしょうか。

開発しているシステムの規模が徐々に大きくなったり、他の開発者から引き継いだりしたときに、「ソースコードのこの部分をいじると副作用で何が起こるか分からないから、怖いので触れたくない」と感じることは意外と多いでしょう。私自身もこういった経験があります。精神衛生上もよろしくなく、「過剰に気を付ける」ことで開発効率も落ちてしまいます。

しかし、きちんと保守されているテストコードがあれば、そのような困った事態になる可能性を下げられます。ソースコードを修正したときにテストを実行すれば、意図しない影響を与えていないのかを確認することができます。テストを動かすだけで「(本来はこうあるべき出力が)こんな値になっているよ」と教えてくれるのです。

また、テストコードは、ドキュメントが整備されなくなった場合に、プログラムの仕様を担保する最後の砦になってくれます。

テストコードを書くときの注意点

テストコードを書く際には、どんなことに気を付けたらいいのでしょうか?

テストコードを書くためには、ソースコード自体も「テストコードを動かす」ことを意識した書き方をすると、テストコードが書きやすくなります。関数を機能ごとに分割しないとテストが書きにくくなるので、多くの処理を一つの関数に詰め込まないなどの意識は必要です。

また、テストコードを書いていくためには、一人だけで頑張るのではなく、一緒に開発する人全員の同意・協力・理解が必要になるでしょう。エンジニアには、ソースコードに手を加える際に一緒にテストコードを直してもらい、エンジニア以外の職種の人にはテストコードを書く意義を理解してもらう必要があるでしょう。

テストコードを書きはじめたばかりのころは、書き方や文化に慣れるまで、それまでより開発速度が一時的に落ちるでしょう。しかし、一時的にコストがかかっても長期的にはメリットが大きいことを理解してもらわないと、職種間で温度差が生じ、トラブルにもなりかねません。

まずは、少しずつでもテストコードを書いてみて、抵抗感をなくしていくことが大切です。いきなり「完璧なテストコードを書こう」と無理すると、長続きせずに挫折してしまう可能性が高いです。

実務の中でも、テストコードを書くことが難しい処理に出くわす場合も少なからずあります。そのようなときでも「必ずテストコードを書かないといけない」と強迫観念にとらわれる必要はありません。手動で結合テストを実施すれば済むものもありますし、処理によってはわざわざテストコードを書く必要がないものもあります。

コストとメリットを考え、必要なもの、可能なものから書いていけばいいのです。無理のない範囲から、テストコードに慣れていきましょう。

参考資料:「50分でわかるテスト駆動開発」

日本でTDDの第一人者といえば、和田卓人(@t_wada)さんの名前がよく挙がります。TDDやテストコードについてインターネットで調べていると、t_wadaさんがまとめた発表資料やスライドを目にすることも多いはず。

最近では、マイクロソフトのイベント「de:code 2017」の発表資料(2017年5月)が、当日のトークもあわせて公開されています。TDDの学習に役立つので、時間を作って一度見てみましょう。

150分でわかるテスト駆動開発 | de:code 2017 | Channel 92

PHPUnitのインストールと準備

テストコードを動かす環境を整えていきましょう。今回はPHPでツールを実行するので、PHPUnitを用いてテストを書いていきます。

PHPUnit – The PHP Testing Framework3

PHPUnitのインストール方法はいくつかあり、公式サイトのマニュアルに記載されています。

なお、この記事では、第一弾で用意したPHP開発環境を前提として説明します。

今日からはじめる休日個人開発 ~ クラウドサービスの選定から、WebサーバでPHPを動かすまで5

コラム:開発環境を最新の状態にする

環境を構築してから時間が経っている方は、脆弱性対策のため、次の手順で最新の状態にしましょう。

$ sudo yum update

カーネル関連のアップデートが含まれている場合は、OSを再起動して、更新した内容を反映させます。

$ sudo reboot

Composer - PHPのパッケージ管理ツール

今回はPHPUnitを、Composerを使ってインストールします。Composerは、PHPでよく使われるパッケージ管理ツールなので、使い方を一緒に覚えてしまいましょう。

6Composer7

Composerでは、使いたいパッケージをJSONで指定することで管理できます。

例えば、GitHubで公開したソースコードを動かすために、他のパッケージがいくつか必要になる場合もあります。そのような場合に、必要なパッケージやそのバージョン情報をJSONファイルに記載して一緒にGitHubへ上げておくことにより、必要なパッケージに依存するパッケージも含め、そのパッケージ構成を管理できます。利用者は動作に必要なパッケージを、Composerを利用して各自の環境へ簡単にインストールできます。

Composerのメインリポジトリが、Packagistです。このサイトに記載されているパッケージは、Composerで利用できます。つまり、ここにパッケージを登録すれば、全世界の人がComposerで利用することができるようになります。

Packagist - The PHP Package Repository9

Composerをインストール

Composerをインストールしていきましょう。公式サイトにある手順に従ってComposerをインストールしていきます。

なお、Composerはroot権限で使わないことが推奨されているので、特に必要性がなければ、sudoの乱用はやめましょう。

10How do I install untrusted packages safely? Is it safe to run Composer as superuser or root?

まず、https://getcomposer.org/installercomposer-setup.phpというファイル名で保存します。

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ ls
composer-setup.php

次に、ダウンロードしたファイルが意図しているものと同じなのか、ハッシュ値から確認します。

$ php -r "if (hash_file('SHA384', 'composer-setup.php') === '669656bab3166a7aff8a7506b8cb2d1c292f042046c5a994c43155c0be6190fa0355160742ab2e1c88d40d5be660b410') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
Installer verified

ダウンロードしたファイルを実行します。ここでは、filenameオプションを付けて、インストールされる実行ファイルのファイル名をcomposerと指定しています。

$ php composer-setup.php --filename=composer
All settings correct for using Composer
Downloading...

Composer (version 1.4.2) successfully installed to: /home/user1/composer
Use it: php composer

composerが生成されています。

$ ls
composer  composer-setup.php

インストールに使ったファイルを削除します。

$ php -r "unlink('composer-setup.php');"
$ ls
composer

最後に、Composerをどのディレクトリからでも使えるよう、移動させます。

$ sudo mv composer /usr/local/bin/

以上で、Composerが利用できるようになりました。

$ composer --version
Composer version 1.4.2 2017-05-17 08:17:52

PHPUnitをComposerでインストール

Composerが用意できたので、PHPUnitをインストールしていきます。

この記事で前提としている、第一弾で構築したパッケージ構成の環境では、PHPUnitに必要なOSのパッケージを事前にインストールしておく必要があります。

$ sudo yum -y install php-xml

その前に、これから作る画像加工ツールを、PHPUnitも含めてまとめて配置する適当なディレクトリを、各自のホームディレクトリに作成しておきましょう。

$ mkdir tool
$ cd tool/

ここではtoolというディレクトリ名を作成しました。以下の作業はここで行っていきます。

それではPHPUnitをインストールしていきましょう。まず、composer.jsonを作成します。このJSONファイルに、Composerで管理・インストールしたいパッケージを記載します。今回はPHPUnitを記載します。

$ vi composer.json
{
    "require-dev": {
        "phpunit/phpunit": "3.7.*"
    }
}

これだけで準備完了です。Composerを使ってインストールしてみましょう。

$ composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 8 installs, 0 updates, 0 removals
  - Installing symfony/yaml (v2.8.24): Downloading (100%)
  - Installing phpunit/php-text-template (1.2.1): Downloading (100%)
  - Installing phpunit/phpunit-mock-objects (1.2.3): Downloading (100%)
  - Installing phpunit/php-timer (1.0.9): Downloading (100%)
  - Installing phpunit/php-file-iterator (1.4.2): Downloading (100%)
  - Installing phpunit/php-token-stream (1.2.2): Downloading (100%)
  - Installing phpunit/php-code-coverage (1.2.18): Downloading (100%)
  - Installing phpunit/phpunit (3.7.38): Downloading (100%)
phpunit/phpunit-mock-objects suggests installing ext-soap (*)
phpunit/php-code-coverage suggests installing ext-xdebug (>=2.0.5)
phpunit/phpunit suggests installing phpunit/php-invoker (~1.1)
Writing lock file
Generating autoload files

これで、./vendor/bin/にPHPUnitがインストールされました。確認してみましょう。

$ ./vendor/bin/phpunit --version
PHPUnit 3.7.38 by Sebastian Bergmann.

PHPUnitでテストコードを動かしてみよう

インストールしたPHPUnitを試しに動かしてみましょう。サンプルのテストコードを記述したファイルを作成します。

テストコードでは、「正解とする値」と、「実際のソースにあるロジックのアウトプットとして出てきた値」が等しいかどうかを比較します。

テストが成功する場合

次のようなSampleTest.phpファイルを作成します。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
  public function testEqual() {
    $expected = 5;   // 期待する正解の値
    $actual = 2 + 3;  // 実際に得られる値
    $this->assertEquals($expected, $actual);
  }
}

このサンプルでは、「2 + 3」の結果が「5」、ということをテストしています。加算している部分は、本来であればプロダクト内にある関数を呼び出すコードに相当します。

これをPHPUnitで動かしてみます。

$ ./vendor/bin/phpunit SampleTest.php
PHPUnit 3.7.38 by Sebastian Bergmann.

.

Time: 19 ms, Memory: 2.25MB

OK (1 test, 1 assertion)

OKとなり、テストが無事に通りました。

テストが失敗する場合

次に、SampleTest.phpを失敗するように書き換え、挙動を確認してみます。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
  public function testEqual()
  {
    $expected = 5;
    $actual = 2 + 4;  // 期待される$expectedの値「5」と異なる
    $this->assertEquals($expected, $actual);
  }
}

期待される値は「5」ですが、「2 + 4」の結果は「6」なので、期待値とは異なります。

$ ./vendor/bin/phpunit SampleTest.php
PHPUnit 3.7.38 by Sebastian Bergmann.

F

Time: 18 ms, Memory: 2.50MB

There was 1 failure:

1) SampleTest::testEqual
Failed asserting that 6 matches expected 5.

/home/user1/phpunit/SampleTest.php:8

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

先ほどと異なり、FAILURESでテストが失敗しました。「8行目で5が期待されているのに実際には6になっている」、と教えてくれています。

このようなテストコードを、ロジックのソースコードを書く際にセットで用意しておくと、リファクタリングや機能追加によって意図しない影響を与えた場合でも、テストを実行すれば不具合を検知することができます。

テストコードの関数名

SampleTest.phpでは、テストコードの関数名をtestEqual()としていました。このようにtestで始まる関数は、PHPUnitが自動的にテストだと判断します。

また、@testアノテーションを使うことにより、testで始まらない関数でもテストとして認識させることができます。これにより、テストの関数名を日本語で付けることもできます。

<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
  /**
   * @test
   */
  public function 値が等しいかどうか()
  {
    $expected = 5;
    $actual = 2 + 4;
    $this->assertEquals($expected, $actual);
  }
}

集中線ツール作りを始めよう

この記事の目的は、テストコードを書きながら簡単な画像加工ツールを作ってみることですが、まずはどんなツールを作るのかを決めましょう。

ブログの記事などで、印象を強くするために写真に集中線が合成されていたり、そういう写真がズームするように何枚も連続で使われたりしているのを見たことありませんか。今回は、そんな集中線付きの画像を自動で生成するツールを作ってみます。

11

この記事で説明するツールで加工できる集中線を追加した画像の例

作成する集中線ツールの仕様を簡単にまとめてみます。

  • 加工したい画像をアップロードできる
  • 画像の右下にコピーライトの文字を重ねることができる
  • 画像の中心に向けて集中線を重ねる(合成する)ことができる
  • 画像の中心に向けてズームになるように任意の枚数に分割してトリミングできる

なお、この記事はテストを書きながら開発するスタイルを大まかに説明するものなので、ツールとして完全なものには仕上げていません。実用には、さらに細かいところを詰めていく必要あります。

集中線ツールのファイル構成

この記事で作成する集中線ツールのファイル構成は、次の図のようになります。

12

先ほどPHPUnitをインストールしたtool配下に、ファイルを配置していきます。

デフォルト設定で、Apache上で動作させたいPHPファイルは、/var/www/html/に配置しなければなりません。開発中のホームディレクトリにあるファイルを、修正するたびにすべてコピーするのは手間がかかります。そこで、今回はシンボリックリンクを作成してしまいましょう1

$ sudo ln -s /home/user1/tool/ /var/www/html/
$ chmod 755 /home/user1/
$ ls -l /var/www/html/
合計 0
lrwxrwxrwx 1 root root 17  X月 XX XX:XX tool -> /home/user1/tool/

これにより、ホームディレクトリにあるtool/の中身を修正すれば、/var/www/html/tool/にもその内容が反映され、ブラウザからアクセスしたときにもその内容が反映された状態になります。

集中線ツールに必要なファイルの準備

今回は既存のフレームワークは使わず、簡単なMVCモデルを自分で用意してみます。ここで準備するのは、以下のファイルです。

  • index.php
  • lib/ …… MVCを構成するファイル群
  • lib/controller.php …… コントローラ
  • lib/model.php …… モデル
  • lib/view.php …… ビュー
  • template/index.tpl …… テンプレート
MVCモデル
UIを持つアプリケーション開発で使われる基本的なモデル。プログラムを、モデル(Model)、ビュー(View)、コントローラ(Controller)の3つの要素に分割し、それぞれビジネスロジック、出力、入力を担当させる。
index.php

集中線ツールにアクセスがあったときに、まず最初に呼び出されるPHPファイルです。この中でControllerを呼び出し、execute関数を実行させます。

<?php
require_once('lib/controller.php');
$controller = new Controller();
$controller->execute();
lib/controller.php

Controllerです。ModelとViewを順に呼び出します。

<?php
class Controller {
  public function __construct()
  {
  }

  /**
  * index.phpから実行される関数
  */
  public function execute()
  {
    require_once('model.php');
    require_once('view.php');
    $modelInstance = new Model();
    $viewInstance = new View();

    $data = $modelInstance->dispatch();
    $viewInstance->display($data);
  }
}
lib/model.php

Modelです。この記事では、実際の画像処理ロジックをここに書いていきます。

<?php
class Model {
  public function __construct()
  {
  }

  public function dispatch()
  {
    // データを処理し、$dataに格納
    $data['msg'] = 'tmp';
    return $data;
  }

  // 動作確認用
  public function test()
  {
    return 'test';
  }
}
lib/view.php

Viewです。Modelで処理された値を用いてテンプレートを呼び出します。

<?php
class View {
  public function __construct()
  {
  }

  /**
   * 画面を表示する
   */
  public function display($data)
  {
    include('template/index.tpl');
  }
}
template/index.tpl

表示に利用されるテンプレートです。

<html>
<body>
<?php echo $data['msg']; ?>
</body>
</html>

以上のファイルを配置すると「tmp」と表示されるページが作成されます。次のURLにアクセスしてみましょう。

http://サーバのIPアドレスまたはドメイン/tool/

Model内で仮に与えているtmpの文字が表示されていることが確認できます。

テストに必要なファイルを準備

集中線ツールに必要なファイルの次には、テストに必要なファイルを追加します。

  • phpunit.xml …… 設定ファイル
  • tests/bootstrap.php …… 最初に実行されるbootstrapファイル
  • tests/ModelTest.php …… 実際のテストコードを記述するファイル
phpunit.xml

PHPUnitが実行されるときの設定をXMLで記載します。

テストコードが配置されているディレクトリや、呼び出すbootstrapファイルを指定しています。

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="./tests/bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
>
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>./tests/</directory>
        </testsuite>
    </testsuites>
</phpunit>
tests/bootstrap.php

それぞれのテストコードでrequireを都度記載しなくても済むように、bootstrapファイル内でrequireしています。この記述により、テストコード内からModelにある関数が呼び出せるようになります。

<?php
require_once(__DIR__ . '/../lib/model.php');
tests/ModelTest.php

実際のテストコードを書いていくファイルです。

<?php
class ModelTest extends PHPUnit_Framework_TestCase
{
  public function testEqual()
  {
    $expected = 'test';
    $this->assertEquals($expected, Model::test());
  }
}

テストの動作確認

toolディレクトリでPHPUnitを実行してみましょう。

テストを実行する際のカレントディレクトリにあるphpunit.xmlが読まれるため、テストの実行時にtests/bootstrap.phpが呼び出され、テスト対象のlib/model.phpが自動的にrequire_onceされる仕組みです。

$ ./vendor/bin/phpunit
PHPUnit 3.7.38 by Sebastian Bergmann.

Configuration read from /home/user1/tool/phpunit.xml

.

Time: 21 ms, Memory: 2.50MB

OK (1 test, 1 assertion)

これで開発に必要な土台は完成です。

ImageMagickを準備

ここで、画像の作成や加工に必要なソフトウェア「ImageMagick」もインストールしておきます。

13Convert, Edit, Or Compose Bitmap Images @ ImageMagick14

実際には、ImageMagickをPHPから利用可能にするネイティブのPHP拡張モジュール「Imagick」を利用します。

15PHP: ImageMagick - Manual - 関数リファレンス16

Imagickは、PHPの拡張モジュールを提供しているPECLでインストールできます。

17PHP: PECLインストール入門 - Manual18

そこで、まずPECLを使えるようにします。あわせて、Imagickに必要なImageMagick本体や、その動作に必要なパッケージをインストールします。

$ sudo yum -y install php-pear php-devel gcc ImageMagick ImageMagick-devel ImageMagick-perl

続いて、PECLでImagickモジュールをインストールします。

$ sudo pecl install imagick

Please provide the prefix of Imagemagick installation [autodetect] :と表示されれば、そのままReturnEnter)キーを押します。

最後に以下のようなメッセージが出ていれば成功です。

Build process completed successfully
Installing '/usr/lib64/php/modules/imagick.so'
Installing '/usr/include/php/ext/imagick/php_imagick_shared.h'
install ok: channel://pecl.php.net/imagick-3.4.3
configuration option "php_ini" is not set to php.ini location
You should add "extension=imagick.so" to php.ini

メッセージにある通り、/etc/php.iniファイルに次の行を追加しましょう。

extension=imagick.so

php.iniファイルの修正を反映させます。

$ systemctl restart httpd.service

以上で、PHPからImageMagickを利用できるようになりました。

集中線ツールの開発1 - テンプレートの表示を調整

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く