旅するえんじにあ - Engineers to Travel -

旅するエンジニアの気まま備忘録

【Parse】 Parse.comのCloudCodeでレコードの保存、取得、変更を行う

さて、前回ParseのCloudCodeについてブログを書きましたが 早速使ってみました。 といっても簡単なものですが保存、検索等よく使うものの簡単な説明です。

もちろんafterSaveやbeforeSave等便利なものは多くあるのですが まずは基本です。

今回はTestって安直なClassを用意しました。 そこに対してSaveして、更にSaveしたものをGetして確認 そこからSaveした内容を変更してみたいと思います。

[TOC]

レコードの保存(Save)

/**
 * save
 */
Parse.Cloud.define("saveTest", function(request, response) {


    var TestClass = Parse.Object.extend('Test');
    var Test = new TestClass();

    var userName = "Ben";
    var age = 18;

    Test.set("name", userName);
    Test.set("age", age);
    Test.save(null, {
        success: function(result) {
            response.success("done!");
        },
        error: function(error) {
            response.error(error);
        }
    });
});

これは基本的なものですね。 userNameとageを定義してそのユーザのデータを作成します。 Test.setにColumn名指定し、そこに入れたいデータですね。 そして最後にsaveするとsetしたデータが作成されます。

実行は以下のコマンドで

curl -X POST
 -H "X-Parse-Application-Id: [Application ID]"
 -H "X-Parse-REST-API-Key: [REST API Key]"
 -H "Content-Type: application/json"
 -d "{}"
 https://api.parse.com/1/functions/saveTest

今回はパラメータの指定はないのでjsonは空にしていますが -dを削っちゃってもOKです。

返り値は以下の通りです。

 # curl -X POST -H "X-Parse-Application-Id: [Application ID]" -H "X-Parse-REST-API-Key: [REST API Key]" -H "Content-Type: application/json" -d "{}" https://api.parse.com/1/functions/saveTest
{"result":"done!"}

成功しているので response.success("done!"); となっているところが実行され、返ってきました。

でもこのコードだと毎回"Ben"ってユーザしか登録されません。 ということでパラメータを渡して登録させましょう。 パラメータを受け取るとコードでは

request.params.xxxx

という形で受け取れます。

コードは以下の通りです。

/**
 * save result.params
 */
Parse.Cloud.define("saveTestRequestParams", function(request, response) {

    var TestClass = Parse.Object.extend('Test');
    var Test = new TestClass();

    var userName = request.params.userName;
    var age = request.params.age;

    Test.set("name", userName);
    Test.set("age", 18);
    Test.save(null, {
        success: function(result) {
            response.success("done!");
        },
        error: function(error) {
            response.error(error);
        }
    });
});

実行は以下です。

curl -X POST
 -H "X-Parse-Application-Id: [Application ID]"
 -H "X-Parse-REST-API-Key: [REST API Key]"
 -H "Content-Type: application/json"
 -d '{"userName": "deadcode", "age": 18"}'
 https://api.parse.com/1/functions/saveTestRequestParams

-dとして空Jsonにしていましたが、jsonにパラメータをつけました。 返り値は以下の通りです。

 # curl -X POST -H "X-Parse-Application-Id: [Application ID]" -H "X-P
arse-REST-API-Key: [REST API Key]" -H "Content-Type: application/json" -d '{"userName":
 "deadcode"}' https://api.parse.com/1/functions/saveTestRequestParams
{"result":"done!"}

さて更に言うと、登録した内容(setした内容)を返したい場合があります。 この場合はsave後にsuccess: function(result)としているところに注目です。 resultには登録した内容が返されます。

以下のようにすると返却内容を見ることができます。

   Test.save(null, {
        success: function(result) {
            response.success(result);
        },
        error: function(error) {
            response.error(error);
        }
    });

上記のように変更して実行してみると返却値は以下のようになります。

# curl -X POST -H "X-Parse-Application-Id: [Application ID]" -H "X-Parse-REST-API-Key: [REST API Key]" -H "Content-Type: application/json" -d '{"userName": "deadcode"}' https://api.parse.com/1/functions/saveTestRequestParams
{"reult":{"__type":"Object","age":18,"className":"Test","createdAt":"2015-05-15T09:05:18.498Z","name":"deadcode","objectId":"cMIr47mjEy","updatedAt":"2015-05-15T09:05:18.498Z"}}

登録された内容がそのまま返却されます。 ここで登録した内容の一部 要はnameとageだけを返却したい場合は以下のようにします。

   Test.save(null, {
        success: function(result) {
            var res = {};
            res.userName = result.get("userName");
            res.age = result.get("age");
            response.success(res);
        },
        error: function(error) {
            response.error(error);
        }
    });

上記のようにすることによって登録した一部の情報を返す事ができます。

レコードの取得(find, first)

では次にsaveした内容をfindしてデータを取得します。 parseではfindとfirstが用意されています。 レコードが一意である場合はfirst, 複数取得する場合はfindを使います。

先ほど取得したデータを取得してみます。 コードは以下の通りです。

/**
 * get find
 */
Parse.Cloud.define("getTestFind", function(request, response) {

    var query = new Parse.Query("Test");

    var userName = request.params.userName;

    query.equalTo("name", userName);
    query.find({
        success: function(results) {
            var res = {};
            res.userName = results[0].get("name");
            res.age = results[0].get("age");
            response.success(res);
        },
        error: function(error) {
            response.error(error);
        }
    })
});

今回findを使ってのデータ取得ですが saveとの違いはquery部分です。 今回はqueryとしてParse.Queryを使ってテーブル(Class)指定をします。

更にそのqueryでequalToを使って検索しています。 ここに関しては色々と使えるのですが、次の時にでも書きます。

また、success: function(results)として受け取ってるresultsに検索結果が配列で返ってきます。 findに関しては一致するレコードを全て取得します。 今回は1レコードしかないので直接配列の0番目を指定しています。

また、返却されたresultsの値を取る場合はgetを使ってカラム名を指定することによって取得できます。

実行してみましょう。

curl -X POST
 -H "X-Parse-Application-Id: [Application ID]"
 -H "X-Parse-REST-API-Key: [REST API Key]"
 -H "Content-Type: application/json"
 -d '{"userName": "deadcode"}'
 https://api.parse.com/1/functions/getTestFind

今回はuserNameを先ほど登録した"deadcode"として指定します。 返り値は以下のようになります。

 # curl -X POST -H "X-Parse-Application-Id: [Application ID]" -H "X-Parse-REST-API-Key: [REST API Key]" -H "Content-Type: application/json" -d '{"userName": "deadcode"}' https://api.parse.com/1/functions/getTestFind

{"result":{"age":18,"userName":"deadcode"}}

ちゃんと返ってきましたね。 firstについてはCakeと同じように1レコードのみ取得してきます。

コードはfindをfirstに変更するだけです。

また、違いとしてはfindは配列で返ってくるのに対して firstは1レコードだけ持ってくる為、getで取得する時にresuls[0]とfindでは配列のキーを指定していましたが findではresults.getという形で取得します。

レコードの変更

次はレコードを変更してみましょう。 既に登録してあるレコードがあるのでそこを変更します。 変更方法はいくつかあるのですが、以下のようなコードを書きました。

/**
 * updateTest
 */
Parse.Cloud.define("updateTest", function(request, response) {

    var query = new Parse.Query("Test");

    query.equalTo("name", request.params.userName);
    query.first({
        success: function(result) {
            var TestClass = Parse.Object.extend("Test");
            var Test = new TestClass();

            result.set("age", request.params.age);
            result.save();

            response.success("done!!");
        },
        error: function(error) {
            response.error(error);
        }
    });
});

手順としてはfirstで変更したいレコードを取得して上書き保存みたいな形で変更しています。 firstで実行して取得したレコードにsetを使ってsaveすることによって上書きができます。

実行してみましょう。

curl -X POST
 -H "X-Parse-Application-Id: [Application ID]"
 -H "X-Parse-REST-API-Key: [REST API Key]"
 -H "Content-Type: application/json"
 -d '{"userName": "deadcode", "age": 20}'
 https://api.parse.com/1/functions/updateTest

返り値は以下の通りです。

# curl -X POST -H "X-Parse-Application-Id: [Application ID]" -H "X-Parse-REST-API-Key: [REST API Key]" -H "Content-Type: application/json" -d '{"userName": "deadcode", "age": 20}' https://api.parse.com/1/functions/updateTest
{"result":"done!!"}

以上でParseのCloudCodeで保存、取得、変更の基本が完了です。 他にも色々とか書きたい事、やってみたい事があるのですが、それは次の機会にでも。

【Parse】 最近流行りのMBaaS BaaSの Parse.com を触ってみた。

Baas MBaaSなんて言葉を最近良く聞くようになりました。 これは(Mobile) Backend as a Service の略称になり IaaSとかPaaSなんかのように自分でバックエンドのコードを書いて実装する必要がありますが BaaSやMBaaSについてはSDKAPIでなんとかしちゃおう的な感じです。

要はクライアントサイドは作れる、View周りのコーディングだってできるというフロントの方々が動的なページが必要だったり、システム部分が必要になったらサーバサイドの開発が必要。

更に言うと、PHPやNode.js、Javaだってできるぜって方でもLinux(でなくてもいいけど)環境を構築できるかというと色々とスキルが必要になってきます。

そこでBaaSです。

今回から何度かに渡って最近使ってるParseについて書いていければなと思います。 さて今回はParseとはなんぞやってところからなのかなと。

一言で言うと、サーバ側のコードを書かずにデータストアへのアクセスからプッシュ通知、アカウント管理機能まで付いている 結構便利なやーつ。

しかもチュートリアルも簡単で直感的操作で簡単にできるっていうのが嬉しい。

だがしかし。

あくまで技術ブログなので少しくらい弄りますよ。 ってことで、Parseはサーバ側にCloudCodeというものを備えており、JavascriptAPIを書くことができる。 ここについて少し書いていこうかと思います。

今回はインストールからCloudCodeにソースをアップして確認するところまで。

まずはParseを登録してCreateAppしておいてください。Appの名前はなんでもいいです。 今回は適当に「test」というアプリを作成しました。 まずはAPIを用意するだけなので、Core(ストレージ)でClassを作ってとかいうのは次にでも。

さてそしたら次はLinux側でParseをインストールします。 もちろんMacUnix環境でも可能です。

$ curl -s https://www.parse.com/downloads/cloud_code/installer.sh | sudo /bin/bash

公式に書いてあることそのままですが、parseは/usr/local/bin/parseにインストールされ 付随しているファイルもないので、アンインストールは/usr/local/bin/parseを削除するだけです。

次に、プロジェクトを作成します。 自分はとりあえず、/home/[ユーザ名]以下にparseというディレクトリを作り、その中で作業をすることにしました。

$ mkdir parse

早速Cloud Codeのセットアップをしていきます。 以下コマンドを実行して対話形式に行っていきます。

$ parse new test

すると、まずは自動的に必要ディレクトリが作成され、Email、Passwordを求められます。 Email,PasswordについてはParseのログイン時のものを入力します。

Creating a new project in directory /home/[user_name]/parse/test
Creating directory /home/[user_name]/parse/test/config
Creating config file /home/[user_name]/parse/test/config/global.json
Creating directory /home/[user_name]/parse/test/cloud
Writing out sample file /home/[user_name]/parse/test/cloud/main.js
Creating directory /home/[user_name]/parse/test/public
Writing out sample file /home/[user_name]/parse/test/public/index.html
Email: deadcode.ben@gmail.com
Password: 

入力が完了するとParseで作ったアプリケーションの一覧が出てきます。 testの他にもaaaaaという適当なアプリも作ったので2つ出てきています。 どのアプリケーションのAPIを作るかなので、今回はtestを選択します。

1:      test
2:      aaaaa
Select an App: 1
Set test as default
Written config for test

これでCloudCodeのセットアップは完了です。

ここから作成されたディレクトリを見て行きましょう。

test
├── cloud
│   └── main.js
├── config
│   └── global.json
└── public
    └── index.html

3 directories, 3 files

少な! 今回お世話になるのは test/cloud/main.jsになります。

こちらを早速開いてみます。

// Use Parse.Cloud.define to define as many cloud functions as you want.
// For example:
Parse.Cloud.define("hello", function(request, response) {
  response.success("Hello world!");
});

既にサンプルコードが書かれていますね。 とりあえずHello world!って表示されそうなものですね。

細かい説明は次にして、早速ちょっと変更してアップしてみましょう。

response.success("Hello world!");

ここの記述を

response.success("Hello parse.com world!");

こんなふうにしてみました。

早速このソースをParseにアップしましょう! まずはtestディレクトリまで移動します。

cd /home/[user_name]/parse/test

そこからDeploy!

$ parse deploy test
Uploading source files
Deploying recent changes to scripts...
Deploying recent changes to hosting...
Finished uploading files
New release is named v1 (using Parse JavaScript SDK v1.4.2)

ここではparse deploy testとアプリケーション名を指定していますが 特に指定しなくても大丈夫です。

ということでこれでアップが完了しました。

次は確認をします。

curl -X POST \
  -H "X-Parse-Application-Id: [Application ID]" \
  -H "X-Parse-REST-API-Key: [REST API Key]" \
  -H "Content-Type: application/json" \
  -d '{}' \
  https://api.parse.com/1/functions/hello

こんな感じでCurlでアクセスしにいきます。 [ApplicationID] [Rest API Key] この二点については新しくAppを作成した時に表示されていました。 覚えてない場合はDocsに行くと、App毎のIDやKeyが書かれた状態での上記コマンドがあるのでそれを使いましょう!

参考:https://parse.com/docs/jp/cloud_code_guide

さて、これを叩くと

[root@localhost test]# curl -X POST \
>   -H "X-Parse-Application-Id: 9hEaqooViTltgUK5lBZrqGmv8kZQyY5MVflITDzQ" \
>   -H "X-Parse-REST-API-Key: CaSYfQGd4tdHZz269qwrOONAeyVvEqxIoxNHcd2r" \
>   -H "Content-Type: application/json" \
>   -d '{}' \
>   https://api.parse.com/1/functions/hello
{"result":"Hello parse.com world!"}

こんな感じで出てきます。 最終行に注目してください。

{"result":"Hello parse.com world!"}

先ほど設定した内容がresultとして出力されました。

こういった形でmain.jsに処理を書くことによって少し複雑な事だってできるのです。

今後はここから更にAppのClassよりデータを取得したり、新規にデータ作成、保存したりするような事を書いていければと! 新しい事を覚えるのは楽しいですねー。

【MySQL】 データベース内のテーブルを全てTRUNCATE

たまにありますよね。 phpMyAdminとか使えば簡単かもしれないんですけど データベース内に数百というテーブルが存在していて、これ全部Truncateかけたいんだけど。。。

show tablesでテーブルリスト作ってそこにtruncateを入れて動かす。 そんなんでもいいんですが、1ライナーでさくっと消してやりましょう。

mysql -u[user] -p[password] [database name] -N -e  'show tables' | xargs -IARG mysql -u[user] -p[password] -e 'truncate table ARG' [database name]

上に書いてある通りです。 show tablesで出てくる各テーブルに対してtruncateつけて実行しているだけです。 これで指定したデータベース内は全てtruncateされます。

あぁ、すっきり。

【vim】検索によるシンタックスハイライトを解除する

別にVimerではないんですが 簡単な修正程度であれば面倒なんでviでやっちゃうときあります。 にわかにちょっとだけコマンド知ってるぜって思いながらやってるのですが。

たまに検索に

/php

なんてコマンドを使うと そのファイル内の検索を行ってくれるのですが そのままだとハイライトが消えないのです。 他のファイル開いてもphpがハイライトされる。

めっちゃ気になるんですが。

ってことで検索してハイライトした部分を解除する方法

:noh

これやればOKです。

せっかくなのでvimの設定にショートカットとして登録しておいたりすると便利ですよね。 例えばESCを2回叩いたらハイライトを消すとかね。 ちょっとESCは多様するので自分はやらないのですが vimrcに以下のように記述するとできます。

nnoremap<ESC><ESC>:nohlsearch<CR>

また、ハイライトについても

set hlsearch

これでハイライトをONにします。

set nohlsearch

これでハイライトをOFF

そこまで使わないとはいえ、知っておくと便利だったので、備忘録のためにも。

【PHP】 苦悩の末導入したPHPUnitとDBUnit

やっと忙しい時期がある程度落ち着いたので 久々の更新です。

もちろんコードも書いていますが やはり運用をしていくうえで必要なのがテストです。

これは初めすごく面倒なのですが、一度やってしまうと テストなしではコードが書けなくなります。 自分も最初これを言われた時には何を言ってんだ。。。 と思ったのですが、いざやってみるとまさに・・・

特に今回は有名フレームワークではなく、 MonobitエンジンにおけるMWFフレームワークという 独自フレームワークに近いものにインストールしていきます。

さて導入から躓いた場所、解決策を書いていきます。

インストールはComposerで行います。

{
    "require-dev": {
        "phpunit/phpunit": "3.7.*",
    "phpunit/dbunit": ">=1.3,<1.4",
        "phpunit/phpunit-skeleton-generator": "*",
    "piece/stagehand-testrunner": "~4.1@dev",
    "davedevelopment/phpmig": "*"
    }
}

今回はvendor以下にインストールを行いました。

次に設定周りです。

PHPUnitではphpunit.xmlを指定することによって 色々な設定ができます。

今回目的としては

「独自フレームワークでDBにテストデータを入れつつ、正常系、異常系のテストを行う」

です。

今更ですがテストの大まかな流れとしては

・必要データをDBに投入 ・必要引数の設定と返り値における期待値を設定 ・methodに問い合わせ期待値と一致するか確認

上記を自動化してくれます。

さて、続きです。

ということで目的からするに必要な情報としては 既に作ってあるmethodを見に行くのでdefineされているPathの読み込みや DBのユーザ、ホスト情報等が必要になってくると思います。 それを設定するのがphpunit.xmlです。

ということで作っていきます。

<phpunit colors="true" verbose="true" bootstrap="[APP_PATH]/mwf/vendor/phpunit/UnitTestEntry.php">
    <php>
        <var name="DB_DSN" value="mysql:dbname=[DBName];host=[HOST]" />
        <var name="DB_USER" value="[DB_USER]" />
        <var name="DB_PASSWD" value="[DB_PASSWD]" />
        <var name="DB_NAME" value="[DB_NAME]" />
        <env name="ENV" value="[ENV]" />
        <ini name="display_errors" value="on" />
    </php>
    <testsuites>
        <testsuite name="Unit Test">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

こんな感じで設定しました。 上から見て行きましょう。

[bootstrap] ここには初期に読み込ませるフレームワークのbootstrapやentryのファイルpathを指定します。

[DB_DSN] mysql:dbname=[DBName];host=[HOST] 上記の形でDB名とホストを指定します。

[DB_USER][DB_PASSWD][DB_NAME] DBのユーザ名パスワード、DBの名前を指定します。

[ENV] Unitテストを走らせる上で設定上色々と変更する場合があるので 環境変数にはPHPUnit等の環境変数を指定します。 これによって後で記述しますが、ConfigやDBManager等に追加が必要です。

さて早速動かそうと思ったところで問題が。 skelgenでテストを作ろうと思ったのだが、mwfフレームワークで引き継ぎを行ったソースの entry.phpに書かれているソースが以下のようなものだった。

define( 'APP_ROOT',          \realpath( \dirname( $_SERVER[ 'SCRIPT_FILENAME' ] ) . '/..' ) );

そうAPP_ROOTが実行した場所のpathを返すようになっている。 このままだと実行phpunit自体のpathが取得され、動かない。 だからといって変更するとプログラム全体のPathが変わり、動かなくなる可能性がある。

それは困るので、entry.phpをこちらで自作してphpunitを動かす時にそちらを読み込むように変更する。 そこで使うのがbootstrapだ。 phpunit.xmlに自作したphpunit用のentry.phpを読み込ませる。

そして無事テストケースが出来上がった。 テストを行う前に、まずDBUnitの設定が必要になる。

要はテスト用に使うテーブルを通常使っているDBと分けてテスト専用DBを使うというところ。 こちらは以下のようなファイルを作って対応した。

また、DBに入れるデータについてはyamlを使うようにした。 もちろんPHPUnitではXMLjsonでのデータも作成できる。

見やすいからymlがいいのかねって話でそうなったんだけどね。

CommonDatabaseTest.php

<?php

require_once "PHPUnit/Extensions/Database/TestCase.php";
require_once "PHPUnit/Extensions/Database/DataSet/YamlDataSet.php";

class CommonDatabaseTest extends PHPUnit_Extensions_Database_TestCase
{

    /**
     * @var PDOのインスタンス生成は、クリーンアップおよびフィクスチャ読み込みのときに一度だけ
     */
    static public $pdo = null;

    /**
     * @var PHPUnit_Extensions_Database_DB_IDatabaseConnection のインスタンス生成は、テストごとに一度だけ
     */
    public $conn = null;

    /**
      * データベースに接続
      */
    public function getConnection()
    {
        if ( $this->conn === null ) {
            if ( self::$pdo == null ) {
                self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
                self::$pdo->query('SET NAMES UTF8');
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_NAME']);
        }

        return $this->conn;
    }

    /**
      * フィクスチャを読み込んで、データベースを初期化する
      *
      * @return PHPUnit_Extensions_Database_DataSet_CompositeDataSet
      */
    public function getDataSet($fixtureFile)
    {
        $fixturePath = APP_ROOT . '/tests/fixtures/' . $fixtureFile;
        return new PHPUnit_Extensions_Database_DataSet_YamlDataSet($fixturePath);
    }
}

PHPUnit_Extensions_Database_TestCaseを継承し、CommonDatabaseTestを作りました。 自分の実装はこちらのファイルに記述するようにします。 今後作るテストファイルもこのCommonDatabaseTestを継承して作るようにします。

getConnectionにはphpunit.xmlで指定した内容が入り getDataSetではfixtureFileのpathを引数に入れてmethod毎にfixtureのファイルを用意するようになる。 要はそのmethod毎にテーブルの出し入れをしつつ、柔軟なテストができるということ。

ここで簡単にディレクトリ構造だけを書いておく。

APP_ROOTとなるディレクトリに対して testsというディレクトリを作った。 この中にあるファイルを読み込んでテストを行うようにする。 また、その他fixtureについてもここに入れるルールとした。

tests以下は以下の通りである。

[root@localhost tests]# tree
.
├── fixtures
│   └── TestDaoModelTest             // ディレクトリ名はテストするファイル名
│       ├── testGetAll.yml           // method毎にymlファイルを作成
│       ├── testGetTestById.yml
│       └── testTestTest.yml
├── model
│   └── dao
│       └── TestDaoModelTest.php     // テストが書かれているファイル
└── sql                              //テストで必要となるだろうテーブルはここに置いておく
    ├── create_table.sql
    └── test.sql

説明はコメントに書いた通りなんだけど、用意するのは大まかに2つ テストに必要なfixtureファイルと実際にテストを行うファイル SQLについては便利かもしれないと思って構築用に用意しているものである。

PHPUnitを動かすとDB内の該当テーブルをTRUNCATEし、対象のfixtureファイルをINSERTしてくれる。 なのでテーブルだけ作っておけばOKなのである。

さてこれで終わりかと思いきや、先ほど作ったCommonDatabaseTest.phpにあるように fixtureファイルを渡してあげる必要がある。 そこについてはテストファイルのsetUpでファイル名とmethod名を取得してあげ、渡すようにした。

TestDaoModelTest.php

<?php

require_once VENDOR_ROOT . '/phpunit/dbunit/CommonDatabaseTest.php';
require_once VENDOR_ROOT . '/phpunit/dbunit/PHPUnit/Extensions/Database/DataSet/YamlDataSet.php';
require_once SCRIPTS_ROOT.'/models/dao/TestDaoModel.class.php';

class TestDaoModelTest extends CommonDatabaseTest
{

    /**
     * @var TestDaoModel
     */
    protected $testDaoModel;
    protected $fixtureFile;


    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp()
    {
        $fileInfo = pathinfo(__FILE__);
        $this->fixtureFile = $fileInfo['filename'] . '/' . $this->getName() . '.yml';
        parent::setUp($this->fixtureFile);
        $this->testDaoModel = new TestDaoModel();
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */
    protected function tearDown()
    {
        parent::tearDown($this->fixtureFile);
    }

    /**
     * @covers TestDaoModel::getAll
     */
    public function testGetTestById()
    {
        $data = [
            'Id' => 200,
            'name' => "Bad Request"
        ];

        $result = $this->testDaoModel->getTestById();
        $this->assertEquals($data, $result, 'error');

    }

    /**
     * @covers TestDaoModel::getAll
     */
    public function testGetAll()
    {
        $testDaoModel = new TestDaoModel();
        // Remove the following lines when you implement this test.
        $data = [
            0 => [
                "id" => 1,
                "name" => "test1",
                "created" => "2015-03-10 11:00:00",
                "modified" => "2015-03-11 11:00:00",
                "deleted" => 0
            ],
            1 => [
                "id"=> 2,
                "name"=> "test2",
                "created"=> "2015-03-10 11:00:00",
                "modified"=> "2015-03-11 11:00:00",
                "deleted"=> 0
            ],
            2 => [
                "id"=> 3,
                "name"=> "test3",
                "created"=> "2015-03-10 11:00:00",
                "modified"=> "2015-03-11 11:00:00",
                "deleted"=> 0
            ]
        ];
        $result = $testDaoModel->getAll();
        // $result = false;
        $this->assertEquals($data, $result, 'error');
        // $this->assertEquals(false, $result, 'error');
    }
}

上記のような形のテストファイルを用意した。

ここでまずは先ほどのfixtureファイルの読み込み部分を見ていく。

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp()
    {
        $fileInfo = pathinfo(__FILE__);
        $this->fixtureFile = $fileInfo['filename'] . '/' . $this->getName() . '.yml';
        parent::setUp($this->fixtureFile);
        $this->testDaoModel = new TestDaoModel();
    }

まずテストが走る前にsetUpが走る。 ここでpathinfo関数を使ってファイル名を取得し、$this->getName()を使ってmethod名を取得している。 これで$this->fixtureFileに対してpathを格納する。 そしてparent::setUpに引数をとして渡してある。 また、毎回クラスのインスタンスを作るのは面倒なので最後にインスタンスを作成させる。

parent::setUpは以下のファイルへ続く

vendor\phpunit\dbunit\PHPUnit\Extensions\Database\TestCase.php

    /**
     * Returns the test dataset.
     *
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    protected abstract function getDataSet($fixturePath);


    /**
     * Performs operation returned by getSetUpOperation().
     */
    protected function setUp($fixturePath)
    {
        parent::setUp();
        $this->databaseTester = NULL;
        $this->getDatabaseTester()->setSetUpOperation($this->getSetUpOperation());
        $this->getDatabaseTester()->setDataSet($this->getDataSet($fixturePath));
        $this->getDatabaseTester()->onSetUp();
    }

ここのsetUpが呼ばれるのでsetDataSetを行っている$this->getDataSetへpathを渡す 更に同じファイル内にあるgetDataSetへも引数として渡してあげる すると、先ほど作ったCommonDatabaseTestのgetDataSetへと渡す事ができる

さてこれでPHPUnit等の設定はある程度完了です。

次にphpunit.xml環境変数を設定しました。 実際に接続にいくときはテストファイルではなく、テスト対象のファイルにアクセスし、そちらのmethodを使うので そちらの接続先を変更する必要があります。

フレームワーク側のDatabaseManager等に各環境ごとの設定が書かれていると思うので そちらにDBの接続情報を追加し、テスト用のDBへ向けてあげるようにします。

これで大体の設定は完了

実際に以下で実行をします。

$ phpunit tests/

これで走らせたのですが、うまく走らなかったです。 ※正直install後にこのブログを書いているのでちょっと他にも設定必要だったかもしれないのですが・・・

さて、何故走らないかというと。 走ってはいるんです。 ただ、テストの途中で止まるのです。

今回作成したテストファイルについては

testGetTestById このmethodではjsonファイルから情報を引っ張りだしているだけです。

testGetAll 問題だったのはこれ。 DBに入っているtestテーブルの内容を全て引っ張り出すという処理です。

処理的には動くのですが、テストの途中でwaitがかかったようにうんともすんとも言わないのです。

PHPUnitに問題があるのか? 独自で実装したyml読み込みが悪いのか? 何か設定が悪いのか?

悩みに悩んでPHPUnitのソースを追っていく事に。

実行した所から処理が流れるところを確認してどうやらPHPUnitは正常な処理をしているはずが TruncateしようとしてるところでMySQLのテーブルがロックされているような挙動が見られました。

まずはMySQLに対してPHPUnitはどういったQueryを投げているか調べました。

MySQLのプロセスリストをリアルタイムで見たかったので以下のコマンドで覗いてみました。

$ watch -n1 'mysql -uroot -e "show full processlist"'

すると2つのプロセスが立ち上がり、一つはstatus sleepとなっており、やはりロックされている状態でした。 にしても何故2つもプロセスが立あがるのだろうと次はQueryを調べることに。 MySQLの設定でQueryログを出すように設定をしました。

/etc/my.cnf

[mysqld]
general-log=TRUE
general-log-file=/var/log/mysql/query.log

上記を追加して/var/log以下にmysqlディレクトリを作成してQueryログを出力することに。

そしてMySQL再起動後、PHPUnitを実行してみると以下ログが出てきました。

27 Connect   root@localhost on
27 Query     select @@version_comment limit 1
27 Query     show full processlist
27 Quit
28 Connect   test@localhost on test
28 Query     SET NAMES UTF8
28 Query     TRUNCATE `test`
28 Query     SHOW COLUMNS FROM `test`
28 Query     SHOW INDEX FROM `test`
28 Query     INSERT INTO `test` (`id`, `name`, `created`, `modified`, `deleted`) VALUES (NULL, 'test1', '2015-03-10 11:00:00', '2015-03-11 11:00:00', '0')
28 Query     INSERT INTO `test` (`id`, `name`, `created`, `modified`, `deleted`) VALUES (NULL, 'test2', '2015-03-10 11:00:00', '2015-03-11 11:00:00', '0')
28 Query     INSERT INTO `test` (`id`, `name`, `created`, `modified`, `deleted`) VALUES (NULL, 'test3', '2015-03-10 13:00:00', '2015-03-11 13:00:00', '0')
28 Query     TRUNCATE `test`
28 Query     SHOW COLUMNS FROM `test`
28 Query     SHOW INDEX FROM `test`
28 Query     INSERT INTO `test` (`id`, `name`, `created`, `modified`, `deleted`) VALUES (NULL, 'test1', '2015-03-10 11:00:00', '2015-03-11 11:00:00', '0')
28 Query     INSERT INTO `test` (`id`, `name`, `created`, `modified`, `deleted`) VALUES (NULL, 'test2', '2015-03-10 11:00:00', '2015-03-11 11:00:00', '0')
28 Query     INSERT INTO `test` (`id`, `name`, `created`, `modified`, `deleted`) VALUES (NULL, 'test3', '2015-03-10 11:00:00', '2015-03-11 11:00:00', '0')
29 Connect   test@localhost on test
29 Query     set autocommit=0
29 Query     SELECT * FROM test
28 Query     TRUNCATE `test`

ID27 についてはshow full processlistを実行しているだけなので置いといて ID28 がPHPUnitでfixtureを入れているログになります。 そしてID29 が実際テストファイルからテスト対象methodを呼び出して問い合わせしているところです。

注目すべきはset autocommit=0というところです。

このコマンドをフレームワーク側で投げていて、autocommitを無効にしていました。

これによって一番最後のTRUNCATE testが実行できず止まっていたようでした。

実際フレームワークにてautocommit無効処理を見つけ、phpunit.xmlに記載している ENVだった場合はその処理を行わないように書き換えました。

これにより無事UnitTestが可能に!

いやーハマった。

これから正常、異常系のテストをどう書くか memcacheやrandを使った時のテストをどうするか等 まだまだ問題は山積みですが、少しずつ解決していこうと思います。

テストないコードは書いていて気持ち悪いし、debugもしずらいので これで晴れてテストし、品質を保持した開発ができるっ!

MWFフレームワークを使った開発をしている人は少ないとは思いますが 独自フレームワークやテストの機構を持っていないフレームワークは数多あると思います。 そんな人の役に立てれば。

さーて次のタスクいくかー

【Git】 GithubにSSHの公開鍵を登録する

まぁ最近SVNじゃなくてGitだよね。 ってことでGit導入してみたはいいけど、SSHで鍵登録とかちょっと頭こんがらがりそうで って最初のうち結構そう思ってた。

公開鍵?秘密鍵

わからんよねー。

公開鍵とか秘密鍵っていうのは鍵交換方式認証で使うファイルで 秘密鍵のファイルってのは自分にしか見られないようにするのが鉄則。 公開鍵は認証を行うサーバに置いとくもの。

この2つの鍵がペアになっていることによってIDとパスワードの代わりをしてくれる。

ってことで早速登録していきましょう。

まずは自分のサーバ側 今回は今まで立てたVirtualBoxのローカルサーバ上で行いました。

まずは~/.sshがあるかないか。 なければ作っていきましょう。

# ssh-keygen -t rsa -C "mail@example.com"

mail@example.comはGitに登録したアドレスでも入れておいてください。 そうすると3つほど聞かれます。

保存場所

パスワード

パスワード(確認)

今回パスワードは飛ばしました。なのでEnter3回タタターンしました。

そうすると2つのファイルが~/.ssh以下にできます。

id_rsa             id_rsa.pub

この2つのファイルが公開鍵と秘密鍵になります。

まずはGithubの設定から「SSH Keys」を選択して「Add SSH key」をします。

TitleとKeyというのが出てくるのでTitleはわかりやすい名前を任意でつけます。

Keyには先ほど作ったファイルのid_rsa.pubをless等で表示させて、中のコードをすべてコピーして貼り付けます。

完了したらAdd key

これだけです。

ただ、これだけだと本当に登録されて疎通できるかわからないので確認しましょう。

# ssh -T git@github.com

少し待つと以下の文が出てくるので「yes」と打ちます

The authenticity of host '[ssh.github.com]:443 ([192.30.252.148]:443)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)?

以下文が表示されれば接続成功です。

Hi xxxx! You've successfully authenticated, but GitHub does not provide shell access.

ただ、繋がらない場合もあります。 一つは鍵作成の時にパスワードを登録した場合 この場合は以下コマンドを実行することによりパスワードを回避できます。

ssh-add ~/.ssh/id_rsa

また、以下のような文が表示され、port 22が使えない場合があります。

ssh: connect to host github.com port 22: Connection timed out

この場合は以下コマンドでportを変更して接続してみてください。

ssh -T -p 443 git@ssh.github.com

これで正常に接続できた場合、443portを使って接続するように設定ファイルを作成します。

# touch ~/.ssh/config
# vi ~/.ssh/config

configには以下設定を書きます。

Host github.com
    User git
    Hostname ssh.github.com
    Port 443
    IdentityFile ~/.ssh/id_rsa

これで接続ができるようになります。

Githubは公式に色々な解決法があるので見ておくといいかもしれないですね。

【MySQL】データベース内のテーブル容量を表示させるquery

今回はMySQLです。 そろそろ環境構築も落ち着いてきて、さてさてソースでも読もうかなって思っている時に、ソースだけじゃわからないようなレガシーなコードなわけですよ。 PHPなんですが$dataだったり、どういう値が入っているかコメントも書かれておらず、マジックナンバーガリガリな感じなんです。 そして併用するのがDB。 こちらについてはテーブル数が100ちょっとで使われてたり使われてなかったり。

直感的ではないプログラムはそんな好きじゃないです。

どのDB使われてるんだろう。 多分これだろうと思ってselectかけてみるもハズレ(ハズレってなんだ…)

そんな時たまに使うのがデータベースの容量からテーブルの容量を出力できるSQLなのです。

今回はどのテーブルにどのくらいの情報が入っているのかを確認するためのSQLになります。

とりあえず適当に作ってあったMySQLのDBはこんな感じです。

mysql> show tables;
+------------------+
| Tables_in_test2  |
+------------------+
| area_code        |
| detail_area_code |
| prefectural_code |
| shop_image       |
| shop_info        |
| test_table       |
| user             |
+------------------+
7 rows in set (0.00 sec)

この中で使っていないテーブがあるのです。

そんな時こんなコマンド打ちます。

select 
table_name, engine, table_rows as tbl_rows, avg_row_length as rlen, 
floor((data_length+index_length)/1024/1024) as allMB,  #総容量
floor((data_length)/1024/1024) as dMB,  #データ容量
floor((index_length)/1024/1024) as iMB   #インデックス容量
from information_schema.tables 
where table_schema=database() 
order by (data_length+index_length) desc;

このselect文を発行するとこんなのが返ってきます。

+------------------+--------+----------+------+-------+------+------+
| table_name       | engine | tbl_rows | rlen | allMB | dMB  | iMB  |
+------------------+--------+----------+------+-------+------+------+
| test_table       | InnoDB |        0 |    0 |     0 |    0 |    0 |
| user             | InnoDB |        4 | 4096 |     0 |    0 |    0 |
| shop_info        | InnoDB |        6 | 2730 |     0 |    0 |    0 |
| shop_image       | InnoDB |        0 |    0 |     0 |    0 |    0 |
| prefectural_code | InnoDB |       47 |  348 |     0 |    0 |    0 |
| detail_area_code | InnoDB |        2 | 8192 |     0 |    0 |    0 |
| area_code        | InnoDB |       68 |  240 |     0 |    0 |    0 |
+------------------+--------+----------+------+-------+------+------+
7 rows in set (0.04 sec)

データ少なすぎてMB単位の表示だと0になっちゃうのですが。。。

tbl_rowsは見れば分かる通りですね。 ここが0になっているテーブルはデータがないものです。

これがあればどのテーブルが使われていてどのテーブルが使われていないかすぐわかりますね。

実はわかったところまではよかったんですが、カラム名がvalueだとかdataって書いてあって何が入ってるか全然わからないんですけどね。 長い戦いになりそうだ。。。