読者です 読者をやめる 読者になる 読者になる

TitaniumMobile勉強記

Web系エンジニア向けのキャリアアドバイザーやってましたが現在はフリーランスで開発含めて色々やってます。技術ネタとしてはRuby/RailsとJavaScript関連(Node.js、Titanium)あたり

もぎゃさんが作ったユニットテストライブラリに変更した

Titanium関連で参考にしている情報源の1つのもぎゃろぐさんのTitaniumの単体テストを読み使いやすそうだったのと、自分が作っているアプリで、いまいち仕様が固まってない所があったのでテスト書きながらそのあたりの仕様を固める意味も含めてちょっと使ってみました。

準備

test library for Titanium Mobileをダウンロードして、適当なディレクトリに配置。(自分は、lib/unittest_for_ti.js という感じにしました)

テストコード書く

自分が作っているアプリはデータ構造とUI部分を比較的綺麗に分けていたこともありテスト対象のクラスに関しては簡単にテスト書けました。

今回対象としたのは、ブログのエントリ情報を管理しているEntryクラス。

このクラスでは任意の日付(フォーマットはUnix時間)以降のエントリがサーバ上に存在するか確認したり、一度取得したエントリ情報をローカルのSQLiteに保存したり、そこから値を得たりということが出来るようにしてます。

new entry = new Entry('blogger');
として

  • find(post_date,number,flg):
  • findOneBY(post_date,flg):

みたいな感じで、ローカルのSQLiteから情報を得られるようにしてりこの部分の仕様についてはすでに固めていたのでまずはこちらについてテストコードを書きました

Titanium.include(Titanium.App.appURLToPath("app://lib/unittest_for_ti.js"));
Titanium.include(Titanium.App.appURLToPath("app://model/entry.js"));
var entry = new Entry("yokota");
var post_date = "2007/08/21 00:09:00";
var post_date2 = "2006/04/30 17:33:00";
var limit_number = 5;
var param = "less_equal";

var find_result = entry.find(post_date,limit_number,param);
var find_one_entry = entry.findOneBy(post_date2,param);
var find_one_entry_miss = entry.findOneBy(post_date,param);

addTest('キャッシュしたエントリの件数',function(){
    mustequal(find_result.length,5,'SQLのlimit句の条件を変数limit_numberで指定しているため、件数としては5になる');
});

addTest('指定した日付のエントリのタイトルを取得',function(){
    mustequal(find_one_entry[0].title,"Web2.0がバブルだったとしても、その「技術」までは失われない。",'findOneByで[{"permalink":"xxx","title":"xxx","html_body":"xxx","post_date":"yyyy/mm/dd hh:mm:ss","blogger":"yokota"}]というJSONが得られる。');
});

addTest('意図的に間違ったエントリのタイトルを取得してテスト失敗させる',function(){
    mustdifferent(find_one_entry[0].title,"Web2.0がバブルだったとしても、その「技術」までは失われない。",'「addしておくと便利な10個のTwitterボット」というタイトルが正しいもの');
});

addTest('指定日付のエントリのタイトルをあえて間違って取得',function(){
    mustdifferent(find_one_entry_miss[0].title,"Web2.0がバブルだったとしても、その「技術」までは失われない。",'「addしておくと便利な10個のTwitterボット」というタイトルが正しいもの');
});

すると、ボタンが4つ表示されます。ためしにあえてテストをパスしないように書いた3番目のボタンをクリックすると

[INFO] start:意図的に間違ったエントリのタイトルを取得してテスト失敗させる
[ERROR] NG:「addしておくと便利な10個のTwitterボット」というタイトルが正しいもの
[ERROR] value:Web2.0がバブルだったとしても、その「技術」までは失われない。
[ERROR] expect:Web2.0がバブルだったとしても、その「技術」までは失われない。
[INFO] end:意図的に間違ったエントリのタイトルを取得してテスト失敗させる

となるし、2番目をクリックすると

[INFO] start:指定した日付のエントリのタイトルを取得
[INFO] OK:findOneByで[{"permalink":"xxx","title":"xxx","html_body":"xxx","post_date":"yyyy/mm/dd hh:mm:ss","blogger":"yokota"}]というJSONが得られる。
[INFO] end:指定した日付のエントリのタイトルを取得

となるので、意図したようにテストコード実行できてます。

問題は任意の日付以降のエントリがサーバ上にあるかどうか確認するgetEntry()というメソッドの実装。現在は下記のようにしているのですが、なんとなくこのメソッドで行っている処理が長すぎるように感じているので、サーバ側の情報確認した後の処理についてテストコード書きながらもう少し仕様をしっかり固めていこうとおもってます。

※その仕様が固まれば、アプリはひとまず完成に近づくかなぁ・・・

var Entry = function(blogger){
  this.blogger = blogger;
  this.config = {
    entry_point:'http://localhost:4567/entry/',
    dbName :"entry",
    db :null,
    sql_param : {
      "newer":{
	"condition":">",
	"orderby":"asc",
	"limit":1
      },
      "older":{
	"condition":"<",
	"orderby":"desc",
	"limit":1
      },
      "less_equal":{
	"condition":"<=",
	"orderby":"desc"
      },
      "greater_equal":{
	"condition":">=",
	"orderby":"asc"
      }
    }
  };
};
Entry.prototype = {
  getEntry:function(last_update){
    if(Titanium.Network.online!==false){
      var self = this;
      var _url = self.config.entry_point
	+ self.blogger
	+ "/"
	+ last_update;

      var xhr = Titanium.Network.createHTTPClient();
      xhr.open('GET',_url);
      xhr.onload = function(){
	var result = this.responseText;
	/*
	 サーバ上に新規エントリがあればその値をevalすると
	 下記のようなJSONが得られる
	 [{
	   "permalink":"xxx",
	   "title":"xxx",
	   "html_body":"xxx",
	   "post_date":"yyyy/mm/dd hh:mm:ss",
	   "blogger":"yokota"
	 }]
	 */
	if(result){
	  var blog_collection = eval(result);
	  self.saveToCache(blog_collection);
	}
	/*
	 サーバから取得したエントリ情報がローカルのキャッシュに
	 すでに存在している場合 変数number_of_entriesは1以上に
	 なるためその値を判定してキャッシュから読み込むか
	 サーバ側から得た情報だけを返すか処理わける
	 */
	var has_new_entry = null;
	var limit = 5;
	var number_of_entries = blog_collection.length -1;
	/*
	 サーバから取得したブログ一覧は、投稿日の古い順に
	 配列に格納するようなAPIにしている。
	 そのため、配列最後のpost_dateを取得することで
	 最終投稿日として設定可能
	 */
	var last_update = blog_collection[number_of_entries].post_date;
	var dd = new Date(blog_collection[number_of_entries].post_date);
	var controller = new Controller();
	if (number_of_entries >= 1){
	  has_new_entry = true;
	  var json = self.find(last_update,limit,"greater_equal");
	}else{
	  has_new_entry = false;
	  var json = self.find(last_update,limit,"less_equal");
	}
	controller.receive(json,self.blogger,has_new_entry);

      };

      xhr.onerror = function(error){
	Titanium.API.info(error);
      };

      xhr.send();

    }else{
      Titanium.API.info('can not establish network connection');
    }
  },
  dbOpen:function(){
    this.config.db = Titanium.Database.open(this.config.dbName);
    return true;
  },
  dbClose :function () {
    this.config.db.close();
    this.config.db = null;
  },
  saveToCache:function(entries) {
    this.dbOpen();
    /*
     保存するキャッシュを読み込んで処理する際に
     JSONフォーマットの方が扱いやすいため、文字列化
     した上でjsonというカラムに保存しておく
     */
    this.config.db.execute('CREATE TABLE IF NOT EXISTS entries(blogger TEXT, permalink TEXT, title TEXT, html_body TEXT, post_date DATE, json TEXT)');
    for (var i=0;i<entries.length;i++) {
      var entry = entries[i];
      var json = JSON.stringify(entry);
      var rows = this.config.db.execute(
        'SELECT post_date FROM entries WHERE post_date = ?',
        entry.post_date
      );
      if ( rows.getRowCount() > 0 ) continue;

      var res = this.config.db.execute(
        'INSERT INTO entries (blogger,permalink,title,html_body,post_date,json) VALUES(?,?,?,?,?,?)',
        entry.blogger,
        entry.permalink,
        entry.title,
        entry.html_body,
        entry.post_date,
	json
      );
      Titanium.API.info('Add to DB');
    }
    this.dbClose();
    return true;
  },

  find:function(post_date,number,param){
    this.dbOpen();
    var sql= "SELECT post_date,json FROM entries WHERE blogger = '"
      + this.blogger
      + "' AND post_date "
      + this.config.sql_param[param].condition
      + " '"
      + post_date
      + "' order by post_date "
      + this.config.sql_param[param].orderby
      + " limit "
      + number;
    Ti.API.info(sql);

    var rows = this.config.db.execute(sql);
    var arr = [];
    while (rows.isValidRow()){
      arr.push(rows.fieldByName("json"));
      rows.next();
    }
    var result = "[" +arr.join(",") + "]";
    this.dbClose();
    return eval(result);
  },
  load:function(post_date){
    this.dbOpen();

    var rows = this.db.execute(
      "SELECT * FROM entries WHERE blogger = '"
	+ this.blogger
	+ "' AND "
	+ "post_date >= '"
	+ post_date
	+ "' order by post_date desc"
    );
    /*
     1.文字列としてJSONが保存されているため、一度配列に代入。
     2.代入されたものを結合した上で、JSONとして利用出来る形で値を返す
     */
    var arr = [];
    while (rows.isValidRow()){
      arr.push(rows.fieldByName("json"));
      rows.next();
    }
    var result = "[" +arr.join(",") + "]";
    this.dbClose();
    return eval(result);
  },
  findOneBy:function(post_date,flg){
    /*
     特定のエントリの前のエントリ(もしくは次のエントリ)を
     取得する際にそのエントリのpost_dateを取得することが出来
     れば、本文の情報はload()で取得できるのでこのメソッドを作成
     */
    var sql= "SELECT post_date,json FROM entries WHERE blogger = '"
      + this.blogger
      + "' AND post_date "
      + this.config.sql_param[flg].condition
      + "'"
      + post_date
      + "' order by post_date "
      + this.config.sql_param[flg].orderby
      + " limit 1";


    this.dbOpen();
    var rows = this.config.db.execute(sql);
    var arr = [];
    while (rows.isValidRow()){
      arr.push(rows.fieldByName("json"));
      rows.next();
    }
    var result = "[" +arr.join(",") + "]";
    this.dbClose();
    return eval(result);

  }
};