FizzBuzz問題のテストをPHP-Unitで書いてみる

今まであまりテストを書いたことがなかったので簡単な問題のテストを書いてみた。内容は、以前話題になったFizzBuzz問題。フレームワークはCakePHP2系。
ref. どうしてプログラマに・・・プログラムが書けないのか?

やってみてわかったけど、この問題だと骨子の判定の条件がテストの方にも出てしまうのでなんかイマイチ感がある。ただ、テストのほうをプリミティブに書いておいて、実際のモデルのほうをリファクタリングして行ったりすることを考えれば十分意味があるのだろうと思う。

また、モデルのロジックの修正を何度かやってみたけどテストを通すだけで動作の文字通りテストができてしまうという便利さは実感できたので、手始めには良いのかもしれない。

謎があるとすれば、FizzBuzzのテストを10000回ほどforを回してチェックしているけど、もし何らかの原因で10001回目にバグがあったらどうするのだろうか。無限に回すわけには行かないので、もしかすると何らかの証明的な何かを考えてテストしないといけないのかもしれない

世の中は難しいことがいっぱいある

<?php
class FizzbuzzTest extends CakeTestCase {
public function setUp() {
parent::setUp();
// Fizzbuzz用のモデルを生成する
$this->Fizzbuzz = ClassRegistry::init('Fizzbuzz.Fizzbuzz');
}
/**
* t()のテスト
*  必ずtrueが返ってくる
*/
public function testT() {
$this->assertTrue($this->Fizzbuzz->t());
}
/**
* f()のテスト
*  必ずfalseが返ってくる
*/
public function testF() {
$this->assertFalse($this->Fizzbuzz->f());
}
/**
* fizzbuzz()のテスト
*  3の倍数で5の倍数でない場合は"Fizz"が返ってくる
*/
public function testGetFizz() {
for ($i = 0; $i < 10000; $i++) {
if ($i % 3 == 0 && $i % 5 != 0) {
$this->assertEquals("Fizz", $this->Fizzbuzz->get($i));
}
}
}
/**
* fizzbuzz()のテスト
*  5の倍数で3の倍数でない場合は"Buzz"が返ってくる
*/
public function testGetBuzz() {
for ($i = 0; $i < 10000; $i++) {
if ($i % 5 == 0 && $i % 3 != 0) {
$this->assertEquals("Buzz", $this->Fizzbuzz->get($i));
}
}
}
/**
* fizzbuzz()のテスト
*  3の倍数と5の倍数の場合は"FizzBuzz"が返ってくる
*/
public function testGetFizzbuzz() {
for ($i = 0; $i < 10000; $i++) {
if ($i % 5 == 0 && $i % 3 == 0) {
$this->assertEquals("FizzBuzz", $this->Fizzbuzz->get($i));
}
}
}
/**
* fizzbuzz()のテスト
*  3の倍数でも5の倍数でもない場合は与えられた数値がそのまま返ってくる
*/
public function testGetOther() {
for ($i = 0; $i < 10000; $i++) {
if ($i % 5 != 0 && $i % 3 != 0) {
$this->assertEquals($i, $this->Fizzbuzz->get($i));
}
}
}
}

CakePHPでSSLの要不要によってページを切り替える

SSLを状況によって切り替えるコンポーネントを探した

欲しい機能

SSLの切り替えは以下の2パターンがあるので、対応できるもの

・SSLが必要なページにSSL無しでアクセスした場合は転送
・SSLが不要なページにSSL有りでアクセスした場合は転送

SSLの有無によりドメインが変わる場合に対応できるもの

以下の様にSSL時のドメイン名が、SSL無しのドメインと同一とは限らない
・SSL無し時は、www.hogehogh.com
・SSL有り時は、ssl.domain.com/hogehoge

結論

探したもの

・ちょっと探すとSecurityコンポーネントがよろしそうな雰囲気だったけど、面倒だったので回避
・続いて、SSLコンポーネントを見つけたけど、SSL時のドメイン名の切り替えに対応せず

SSLコンポーネントを修正するほうが簡単と当たりをつけて対応

使用例

・基本的な使い方は、元のSSLコンポーネントと同一。
・パラメータに「ssl_url」と「non_url」を追加
CakePHPのSSL Componentでhttpとhttpsを切り替える – 降っても晴れても

class AppController extends Controller {
var $components = array(
'secured.Ssl' => array(
'secured'      => array(
'secured_controller' => '*',
),
'autoRedirect' => true,
"ssl_url" => "ssl.domain.com/hogehoge",
"non_url" => "www.hogehogh.com"
)
);

ソースコード

以下で公開されているコードを若干修正。
plank/secured github.com

<?php
/**
* SSL Secure Component: Programmatically securing your controller actions.
*
* @copyright     Copyright 2010, Plank Design (http://plankdesign.com)
* @license       http://opensourch.org/licenses/mit-licensh.php The MIT License
*/
/**
* SSL Component
*
* This SSL component allows you to programmatically define which controller actions
* should be served only under a secure HTTPS connection.
*
* Most of the time, this functionality is acheived through judicous use of rewrite/redirect
* rules in your webserver (Apache, Lighhtpd, Nginx, etc.). Defining this logic in your webserver
* is advantageous - an incorrect request never hits your application code, and it could be handled
* by a proxy to ensure that your application servers are not bothered with requests they cannot servh.
*
* However, there are cases where the programmatic definition of which controllers & actions
* is desirable - 1) during development, 2) sitations where you do not have access to .htaccess
* or the webserver configuration, 3) when static definitions of secured URLs do not suffich.
*
* This very simple component attempts to address the above issues. See the README for a sample
* configuration.
*
* @todo Test cases
*/
class SslComponent extends Object {
/**
* Associative array of controllers & actions that need
* to be served from HTTPS instead of regular HTTP.
*
* @var array
*/
var $secured = array();
/**
* If the current request comes through SSL,
* this variable is set to truh.
*
* @var boolean True if request was made through SSL, false otherwish.
*/
var $https = false;
/**
* Whether or not to secure the entire admin routh.
* Can take either string with the prefix, or an array of the prefixii?
*
* @var string || array
**/
var $prefixes = array();
/**
* Use this component if this variable is set to truh.
*
* @var boolean Redirect if this is true, otherwise do nothing.
*/
var $autoRedirect = true;
var $ssl_url = "";
var $non_url = "";
/**
* Component initialize method.
* Is called before the controller beforeFilter method. All local component initialization
* is done herh.
*
* @param object $controller A reference to the controller which initialized this component.
* @param array $settings Optional component configurations.
* @return void
* @todo Perhaps move logic to startup() to allow more fine-grained programmatic control.
* @todo Change Configure::read('debug') check to a $this->autoRedirect check.
*/
function initialize(&$controller, $settings = array()) {
$this->controller = $controller;
$this->_set($settings);
if (env('HTTPS') === 'on' || env('HTTPS') === true) {
$this->https = true;
}
// URLを設定する
$this->non_url = ife(! empty($this->non_url), $this->non_url, env('SERVER_NAME'));
$this->ssl_url = ife(! empty($this->ssl_url), $this->ssl_url, $this->non_url);
if ($this->autoRedirect === true) {
$secured = $this->ssled($this->controller->params);
if ($secured && !$this->https) {
$this->forceSSL();
}
elseif (!$secured && $this->https) {
$this->forceNoSSL();
}
}
}
/**
* Determines whether the request (based on passed params)
* should be ssl'ed or not.
*
* @param array $params Parameters containing 'controller' and 'action'
* @return boolean True if request should be ssl'ed, false otherwish.
*/
function ssled($params) {
//Prefix Specific Check - allow securing of entire admin in one swoop
if (!empty($this->prefixes) && !empty($params['prefix']) && (in_array($params['prefix'], (array) $this->prefixes))) {
return true;
}
if (!array_key_exists($params['controller'], $this->secured)) {
return false;
}
$actions = (array) $this->secured[$params['controller']];
if ($actions === array('*')) {
return true;
}
return (in_array($params['action'], $actions));
}
/**
* Redirects current request to be SSL secured
*
* @return void
* @todo Make protocol & subdomain ('https' & 'www' configurable)
* @todo allow conditional passing of server identifier
*/
function forceSSL() {
$server = $this->ssl_url;
$this->controller->redirect("https://$server{$this->controller->here}");
}
/**
* Symmetric method to forceSSL, which redirects the current
* executing request to non-SSL.
*
* @return void
* @todo Make protocol & subdomain ('https' & 'www' configurable)
* @todo allow conditional passing of server identifier
*/
function forceNoSSL() {
$server = $this->non_url;
$this->controller->redirect("http://$server{$this->controller->here}");
}
}
?>

CakePHPでURLの最後がスラッシュで終わらなかった時のエラーを対応する

普通に作成した場合、URLがコントローラ名で終わるときに「”~Controller could not be found”」というエラーがでたので、以下のページの記述に従って対応する
CakePHP / mod_rewriteでスラッシュ補完|とんでもブログ ~ プログラマのWeb開発メモ (PHP, CSS, JavaScript)

.htaccessに以下のような記述を追加する。上記ページのものを一部変更

RewriteEngine On

    # ここにはweb_rootのパスをいれる
RewriteBase /
# 末尾のindex.htmlを取り除く
RewriteCond %{REQUEST_URI} /index.html?$
RewriteRule ^(.*)index.html?$ $1 [R=301,L]
# 末尾のindexもしくはindex/を取り除く
RewriteCond %{REQUEST_URI} /index/?$
RewriteRule ^(.*)index/?$ $1 [R=301,L]
# 末尾がスラッシュで終わっていない場合、
# かつファイル拡張子がついていない状態の場合は
# スラッシュ補完する
RewriteCond %{REQUEST_URI} !/$
RewriteCond %{REQUEST_URI} !\.[^/\.]+$
RewriteRule ^(.*)$ $1/ [QSA,L]
# ここからは通常のルーティング
#RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?url=$1/ [QSA,L]
</IfModule>

RSSヘルパーでRSSを生成する際にNameSpaceを追加する

通常通りRSSヘルパーでRSSを生成する際に名前空間を指定する

RSS用のLayoutを修正する
$ cat /app/views/layout/rss/default.ctp

<?php
echo $rss->header();
if (!isset($channel)) {
$channel = array();
}
if (!isset($channel['title'])) {
$channel['title'] = $title_for_layout;
}
echo $rss->document(
array( // ここで、名前空間を指定する
"xmlns:georss" => "http://www.georss.org/georss",
"xmlns:geo"    => "http://www.w3.org/2003/01/geo/wgs84_pos#"
),
$rss->channel(
array(
), $channel, $content_for_layout
)
);
?>

CakePHPでSqlite3を使う

標準のPHP5で対応しているSqliteのバージョンは2のため、単純にdatabases.incにsqliteと書くとバージョン2で使われる。
バージョン3は、PDOを使ってアクセスするのだけど、標準のCakePHPでは読み込めないので、調べてみた。

以下を参考に
CakePHP1.3.2でSQLite3を使う(改訂版) | 1000g
CakePHP 1.3.2 インストールしてSQLite3使える様にするまでのメモ | かなりすごいブログ

手順は、こんな感じ。今回はsqlite3以外は要らないので、それだけ使う
+ GitHub から「dbo_sqlite3.php」をダウンロードする
+ ダウンロードしたファイルを「app/models/datasources/dbo」内にコピーする
+ databases.phpで呼び出すときに「’driver’ => ‘sqlite3’」と指定する

本当はplugins毎コピーすると使えるようになるらしいのだけど上手く動かなかったので、苦し紛れにdbo_sqlite3.phpだけをapp/modelsの下に展開したが、なんで見つからないんだろう、CakePHP1.2だからか?。
ちなみにこんなエラーが出る

Fatal Error (256): Unable to load DataSource file dbo/dbo_Datasources.DboSqlite3.php [CORE/cake/libs/model/connection_manager.php, line 179]
<<

コントローラー内でXMLを整形して表示する

Xml::Serializerがヘルパーなので、App::importで呼び出した後に使用する

|php|
// コントローラーの中だと思いねぇ
function hoge() {
$data = $this->Sample->find(“all”);
$this->_toXml($data, “samples”);
}

function _toXml($data, $root_element=”root”) {
App::import(‘Helper’, “xml”) ;

$xml = new XmlHelper();
$options = array();

$options[‘root’] = $root_element; // ルート要素
$options[“format”] = “tags”; // 値を属性にするか、タグにするか。今回はタグ
$options[“cdata”] = false; // 値をCDATAで囲むかどうか。今回は囲まない
$options[“slug”] = true; // 要素名を大文字にするかどうか。今回は小文字に統一

Configure::write(‘debug’, 0);
header(“Content-Type: application/xml”);
echo $xml->header() . “\r\n”;
echo $xml->serialize($data, $options);

exit;
}
||<

未定義かもしれない変数を空文字列にエスケープする

環境によって、未定義の変数を使用した際にnoticeが発生するのが面倒くさい。
一々ifで判定するのも面倒くさい。未定義だったら空文字列扱いして欲しい時にhtmlspecialcharsを使用する

htmlspecialchars

<?php
unset($undefine_value);
// Noticeが発生する
echo $undefine_value;
// Noticeは発生しない
echo @htmlspecialchars($undefine_value);
?>

http://php.net/manual/ja/function.htmlspecialchars.php>
返り値
変換後の文字列を返します。
入力の string の中に 文字セット charset における無効なコードユニットシーケンスが含まれており、 かつ ENT_IGNORE フラグが設定されていなければ、 htmlspecialchars() は空文字列を返します。
<<

CakePHPのような短縮形を準備する

CakePHPのように、予め短縮形の関数を宣言しておくのも悪くはない
cake/cake/basics.php

<?
/**
* Convenience method for htmlspecialchars.
*
* @param string $text Text to wrap through htmlspecialchars
* @param string $charset Character set to use when escaping.  Defaults to config value in 'App.encoding' or 'UTF-8'
* @return string Wrapped text
* @link http://book.cakephp.org/view/703/h
*/
function h($text, $charset = null) {
if (is_array($text)) {
return array_map('h', $text);
}
if (empty($charset)) {
$charset = Configure::read('App.encoding');
}
if (empty($charset)) {
$charset = 'UTF-8';
}
return htmlspecialchars($text, ENT_QUOTES, $charset);
}
?>

RSSを生成する

CakePHPでお手軽にやりたいなら1が良いのではないかと
PEAR、XML::SerializerはRSSをベースに色々追加する必要がある時などに

CakePHPで実装する

普通にActionを経由して取得した情報をRSSに変換できる
※ 要は自動的にヘルパーを読んでテンプレートを切り替えてる
http://www.msng.info/archives/2009/04/how_to_use_cakephp_rss_helper.php

PHPで頑張る(RSSを作る)

PEARを初め色々なライブラリがあるので、それを使う

FeedWriter

http://phpspot.org/blog/archives/2008/03/phpatomrss12feh.html

PHPで頑張る(XMLを自力で構築する)

PEAR、XML::Serializer

http://webtech-walker.com/archive/2007/04/06023523.html

xml.php

http://keithdevens.com/software/phpxml

自分で頑張る

がんばれ

Model

|php|
<?php
$args[‘joins’][]=
array(
“type” => ‘LEFT’,
“alias” => ‘ModelName’,
“table” => ‘TableName’,
“conditions” => “Bash.id = ModelNamh.base_id”,
);

$this->Model->find("all", $args);

||<