2011年12月10日 星期六

My iOS UI Automation Testing(自動化測試)

PastedGraphic-1
自動化測試是程式開發中,不可或缺的一環,而 xcode Instruments 裡面的 Automation tool ,便是 iOS UI 的自動化測試工具。
從 Automation tool 的測試腳本中,可以看得出來編譯完成的 APP,是符合 DOM(Document Object Model) 的。所以測試腳本,也很理所當然找了它最老的夥伴 - Javascript 來調用。

往下閱讀之前,建議一定要先看過...

DOM 雖然強大,但寫測試時卻又嫌囉嗦,要準確取得元件是一件很麻煩的事,看完上述連結,可以知道有兩種取得元件的方式:
  1. 用位置來取得,例如「window.buttons()[0]」,指的是第一個建立出來的 button。
  2. 用「name」來取得,例如「window.buttons()[“login”]」。
第一種方式,元件數量少還好,元件一多,會很容易指錯,而且測試程式碼可讀性幾乎是0,根本不知道到底取得的是哪一個元件。
第二種方式是比較理想的,但必須在 xib 上,或是 Objective-C 程式碼中,另外指定 accessibilityLabel 來命名 DOM 這邊所謂的「name」,也是挺麻煩的。
上述連結有這兩種方式的圖文說明。

突然想到之前寫 RSpec(Ruby 的自動測試框架) 測試 RoR 的時候,所有的操作,都只需要直接使用「肉眼所看到的字」,便可正確取得元件的文字或是進行操作(如點擊),於是便動手進行實作,簡化測試的程式。

https://github.com/alexvollmer/tuneup_js
借用了上述的測試框架,將自己寫的整合了進去,主要寫了三個 function
function haveContent(text) {

    var elements = window.elements();

    

    for (i = 0; i < elements.length; i++) {

        var element = elements[i];

        if (element.name() == text && element.isValid()) {

            return true;

        }

    }

    return false;

}

 

function click_button(name) {

    var buttons = window.buttons();

    

    for (i = 0; i < buttons.length; i++) {

        var button = buttons[i];

        if (button.name() == name) {

            button.tap();

        }

    }

}

 

function fill_in(name, text) {

    var textFields = window.textFields();

    

    for (i = 0; i < textFields.length; i++) {

        var textField = textFields[i];

        if (textField.name() == name) {

            textField.setValue(text);

        }

    }

}

haveContent 與 click_button 這兩個 function,只需指定肉眼看得到的文字就好,例如
test("Clear Hello", function(app, target) {

     click_button("Clear All");

     assertFalse(haveContent("Hello World"));

});

fill_in 則還是得在 xib 上,或是  Objective-C 程式碼中,另外指定 accessibilityLabel ,讓測試程式可以準確的指定到輸入框中,例如
test("Say Hello", function(app, target) {

     fill_in("name", "World");

     click_button("Say Hello");

     assertTrue(haveContent("Hello World"));

});

其實這三個 function 就可以搞定 95% 的測試了,其餘等需要時再補上,提供完整的範例下載

其它參考資源:

2011年11月27日 星期日

關於 Objective-C memory

兩個問題:
  1. 一個app在mobile device上使用了多少記憶體可能導致crash?
  2. 我們要如何看到我們的app使用記憶體的狀況?
在回答這兩個問題之前,再特別強調一個觀念:
「在Objective-C裡面指的memory leak,只是產生了一個程式再也無法用任何方式回收的記憶體空間,其實它沒有那麼恐怖!(指的是它並非是造成crash的主因)」
為何我特別標註第一句話,因為在通用語言的定義中,指的是「沒有任何程式碼來回收的記憶體空間,都叫做memory leak」,事實上也是如此,真正恐怖的不是Objective-C指的memory leak,而是你沒有好好控管你的程式,導致一堆記憶體空間雖然可以用程式回收,但你卻沒有在適當的時機回收,這種memory leak,是你用xcode(Objective-C IDE)的Leaks工具也抓不出來的!
更詳細的memory leak的例子,可以參考這個連結http://www.shopify.com/technology/4321572-most-memory-leaks-are-good
這又回到了上面第二個問題,我們即便知道了記憶體在mobile device上是很珍貴的,但我們如果不知道自己寫的app是如何使用記憶體的,那我們又怎麼規劃最適當的記憶體使用方式?(xcode 4.2預設已經是ARC(Automatic Reference Counting),理論上並不會再出現Objective-C指的memory leak,但仍會出現通用語言定義的memory leak)
先回答第二個問題,其實xcode(instruments)內建的allocations工具就可以觀察記憶體使用狀況了。
再回到第一個問題,「一個app在mobile device上使用了多少記憶體可能導致crash」
記憶體使用本來就是作業系統動態管理的,沒有「明確的指標」是正常的,但一定有所謂的理論危險值,而就我查到的資料,雖然沒有人給100%的肯定答案,不過仍有一些人有推論值(這部份可以隨著我們觀察的經驗,越來越精準):
  • iPhone3G:14MB進入危險邊緣、20MB肯定crashl
  • iPhone3GS:iOS改為更動態的分配記憶體,ram也較大(256mb),較不容易發生,有人推論是iPhone3G的四倍左右,也就是56-80MB
  • ipad:ram規格同上
  • iPhone4:沒查到相關資料,但總之iphone4的ram是512mb,然後作業系統iOS4~5大概佔1xx,我猜超過200mb就進入危險邊緣。
  • ipad2:ram規格同上

參考資源








2011年11月7日 星期一

解決 iOS5 / xcode 4.2 導致第三方函式庫無法使用的問題

由於 iOS5 SDK 將 retain, release, autorelease, dealloc…等記憶體管理的指令都拿掉了,對後進開發者雖然是一大福音,但此舉卻也導致許多第三方的函式庫無法正常使用,最好的解決方案就是告訴 IDE 不要對這些第三方的函式庫進行 Automatic Reference Counting(ARC)。
到 target > build phases > compile sources,將有用到這個記憶體管理的檔案,全部加上
-fno-objc-arc

img

如此一來,便可正常引用第三方的函式庫了。

Note : 相反則是 -fobjc-arc

2011年11月5日 星期六

Objective-C Protocol 觀念

在 http://www.otierney.net/objective-c.html.zh-tw.big5 裡,其實就已經很清楚的提到 Objective-C 的 Protocol 與 Java 的 Interface 相同,底下以一個例子說明。
我們有個 Hero,職業是巫師,而這個職業有火球技能。火球技能的傷害算法是
火球等級 * 火球基礎傷害 * 職業加成倍率

那麼身為 Hero 的火球技能,必須知道 Hero 現在的職業加成倍率為何才對。如果 Hero 轉職了,職業加成倍率也就不同了。所以每當火球技能引發傷害時,都得動態問問看 Hero 現在的職業加成倍率。
在 Objective-C 裡面會這樣實作
Wizard.h
#import <Foundation/Foundation.h>
#import "FireBolt.h"
 
@interface Wizard : NSObject <MagicDamage> {
    FireBolt* fireBolt;
    int intelligence;
    int level;
}
 
-(void) castFireBolt:(id) target;
 
@end


Wizard.m
#import "Wizard.h"
 
@implementation Wizard
 
- (Wizard*) init
{
    self = [super init];
    
    if (self) {
        intelligence = 15;
        level = 5;
        
        FireBolt *tmpFireBolt = [[FireBolt alloc] init];
        tmpFireBolt.delegate = self;
        fireBolt = tmpFireBolt;
    }
    
    return self;
 
}
 
-(void) castFireBolt:(id) target
{
    [fireBolt cast:target];
}
 
-(double) getRate
{
    return intelligence * level;
}
@end


FireBolt.h
#import <Foundation/Foundation.h>
 
@protocol MagicDamage
 
-(double) getRate;
 
@end
 
@interface FireBolt : NSObject {
    double baseDamage;
    int level;
}
 
@property (nonatomic, strong) id <MagicDamage> delegate;
 
-(void) cast:(id) target;
 
@end
FireBolt.m
#import "FireBolt.h"
#import "Enemy.h"
 
@implementation FireBolt
 
@synthesize delegate;
 
- (FireBolt*) init
{
    self = [super init];
    
    if (self) {
        level = 1;
        baseDamage = 10;
    }
    
    return self;
    
}
 
-(void) cast:(Enemy*) target
{
    double rate = [self.delegate getRate];
    double damage = rate * baseDamage * level;
    NSLog(@"%@ 被攻擊了!受到 %f 的傷害", target.Name, damage);
    target.Life -= damage;
    NSLog(@"%@ 的生命剩下 %d", target.Name, target.Life);
}
 
@end


Enemy.h
#import <Foundation/Foundation.h>
 
@interface Enemy : NSObject
 
@property (nonatomic) int Life;
@property (nonatomic, retain) NSString* Name;
 
- (Enemy*) initWithName:(NSString*) name;
 
@end


Enemy.m
#import "Enemy.h"
 
@implementation Enemy
 
@synthesize Life, Name;
 
- (Enemy*) initWithName:(NSString*) name
{
    self = [super init];
    
    if (self) {
        self.Name = name;
        self.Life = 5000;
    }
    
    return self;
    
}
 
@end


main.m
#import "Wizard.h"
#import "Enemy.h"
 
int main( int argc, const char *argv[] ) {
    Enemy *dan = [[Enemy alloc] initWithName:@"Dan"]; 
    Wizard *zephyr = [[Wizard alloc] init];
    [zephyr castFireBolt:dan];
    [zephyr castFireBolt:dan];
    [zephyr castFireBolt:dan];
    [zephyr castFireBolt:dan];
    [zephyr castFireBolt:dan];
    
    return 0;
}


Output
2011-11-05 17:31:53.595 test[886:f803] Dan 被攻擊了!受到 750.000000 的傷害
2011-11-05 17:31:53.596 test[886:f803] Dan 的生命剩下 4250
2011-11-05 17:31:53.597 test[886:f803] Dan 被攻擊了!受到 750.000000 的傷害
2011-11-05 17:31:53.597 test[886:f803] Dan 的生命剩下 3500
2011-11-05 17:31:53.598 test[886:f803] Dan 被攻擊了!受到 750.000000 的傷害
2011-11-05 17:31:53.598 test[886:f803] Dan 的生命剩下 2750
2011-11-05 17:31:53.599 test[886:f803] Dan 被攻擊了!受到 750.000000 的傷害
2011-11-05 17:31:53.599 test[886:f803] Dan 的生命剩下 2000
2011-11-05 17:31:53.600 test[886:f803] Dan 被攻擊了!受到 750.000000 的傷害
2011-11-05 17:31:53.600 test[886:f803] Dan 的生命剩下 1250

2011年10月31日 星期一

在 ubuntu 上的 php 中 exec python 腳本

ubuntu 裡的權限管理非常的嚴格,常常造成子程序沒有權限執行,而 exec 指令中雖然可以加上 sudo,但 ubuntu 的 sudo 仍有「互動式輸入密碼」的問題。
比較好的解法,便是讓 sudo 不需要輸入密碼,而詳細的解法可以參考底下的連結

 

2012/10/15 更新

若要完全取消 sudo 的密碼,可以試試

sudo visudo

# %sudo ALL=(ALL:ALL) ALL
%sudo ALL=(ALL:ALL) NOPASSWD: ALL

2011年10月23日 星期日

用 python 蒙太奇(Montage)、馬賽克(Mosaic) 你的圖片

angelbaby.mosaicangelbaby 

蒙太奇一下我的女神(Angelbaby),這是用 1619 張圖片,縮成 5x5 pixel,且一張小圖最多只能使用 100 次拼湊出來的效果。

 

這是原程式碼

https://github.com/sameeptandon/python-mosaic

 

原程式碼提供的蒙太奇少了兩樣我想要的功能:

  1. 可限定一張小圖可使用的次數。(原程式碼永遠是挑選最像的那張小圖)
  2. 放入小圖時,他是規律性的由上至下,由左至右的放入,而不是隨機跳動。(這功能必須先有上述功能才有意義)

所以動手改了一下,

檔案下載 mosaic.py

使用方式如下:

image

主要參數紅框由左至右的意義是:

  1. 目標圖片
  2. 圖片資料夾
  3. 目標圖片切割成每小塊的寬跟高
  4. 圖片縮小成小圖後的寬跟高
  5. 一張小圖可用的次數(但如果小圖不夠多,則會重新計數)

 

成果要越像,那麼

  1. 圖片資料夾越多圖片越好
  2. 寬跟高越小越好
  3. 一張小圖可用次數越高越好

 

以上

2011年10月10日 星期一

Facebook 非讚不可 php 語法

image

試了一下網路上「非讚不可」的 fbml 語法,是利用

 
<fb:visible-to-connection>

來達成的,但卻可以從原始碼中看到隱藏的內容。

 

看了一下 facebook-php-sdk,其實也可以用這樣的方式達成

 
require_once "lib/facebook.php";
 
$app_id = "";
$app_secret = "";
 
$facebook = new Facebook(array(
    'appId' => $app_id,
    'secret' => $app_secret,
    'cookie' => true
));
 
$user = $facebook->getUser();
$signed_request = $facebook->getSignedRequest();
 
$is_fan = false;
 
if ($signed_request) {
    if($signed_request['page']['liked']) {
        $is_fan = true;
    }
}
 
if ($is_fan) {
    include('fan.php');
} else {
    include('guest.php');
}

 

 

在這邊看到一樣的方法,他寫的比較詳細!

http://nocturnsoft.com/devblog/?p=526

2011年9月27日 星期二

Ruby on Rails 發佈 Twitter 消息

image

圖片來源:https://dev.twitter.com/

 

流程大綱

要在 Twitter 發佈消息,要做幾件事情:

  1. 註冊一個 Twitter 帳號
  2. 用 Twitter 帳號申請 Twitter APP
  3. 將 Twitter APP 權限調整成 「Read and Write」
  4. 在 Ruby on Rails 透過 Twitter OAuth 認證機制取得 access_token
  5. 利用 Ruby on Rails 用 access_token 呼叫 Twitter API 進行發佈

 

參考資訊

https://github.com/oauth/oauth-ruby

https://github.com/twitter4r/twitter4r-core

http://twitter4r.rubyforge.org/

 

流程說明,直接從「用 Twitter 帳號申請 Twitter APP」開始

登入你的 Twitter ,在右下方找到「開發人員」

image

 

一樣在右下方,點擊「Create an app」

image

 

填寫 APP 所需資訊,在這邊 Web Site / Callback URL 的 host:port 是填寫你自己的網站位址(測試用的話,填 127.0.0.1:3000 也可以)

PS:"myapp" 若跟人家衝突到,就自己亂 key 一個名字吧,實際上我是 key “myapp1234”

image

 

填好之後,打勾按下同意

image

 

按下 Settings

image

 

將 Access 的 permission 更改為 Read and Write 按下送出

image

 

image

 

回到 Details

image

 

把 Key 跟 Secret 記下來

image

 

接著產生一個 RoR 的專案

SNAGHTML240def4

 

在 GemFile 裡加上這兩行

image

 

下達 bundle install

SNAGHTML242353e

 

接著下達 rails g controller twitter

SNAGHTML243dd11

 

打開 controller twitter_controller.rb 貼上以下程式碼

image 

class TwitterController < ApplicationController
  def consumer
    consumer = OAuth::Consumer.new('你剛剛申請到的 Key ', '你剛剛申請到的 Secret', {
      :site => 'https://api.twitter.com',
      :authorize_path => '/oauth/authorize'
    })
    return consumer
  end
  
  def login
    request_token = consumer.get_request_token(:oauth_callback => File.join(request.protocol + request.host_with_port, 'twitter/callback'))
    session[:request_token] = request_token.token
    session[:request_token_secret] = request_token.secret
    redirect_to request_token.authorize_url
  end
  
  def callback
    request_token = OAuth::RequestToken.new(consumer, session[:request_token], session[:request_token_secret])
    access_token = request_token.get_access_token(:oauth_verifier => params[:oauth_verifier])
    client = Twitter::Client.new(:oauth_access => {
      :key => access_token.params[:oauth_token], :secret => access_token.params[:oauth_token_secret]
    })
    client.status(:post, '你要發佈的消息')
    render :text => access_token.params
  end
end

 

到 config/routes.rb 把最下面一行註解拿掉

image

 

接著打開 rails server

SNAGHTML24df66a

 

打開瀏覽器輸入底下網址

image

 

授權應用程式

image

 

稍等一下會跳轉到 callback 頁面

image

 

此時到 Twitter 上看,就可以看到消息成功發佈了!

image

 

 

PS:若你要發佈的消息為中文,記得把檔案編碼改為 utf-8 並在檔案上方加入 「#encoding: utf-8」 喔!

image

image

2011年9月25日 星期日

Windows Phone 一行程式都不用寫的氣泡遊戲

SNAGHTMLd07ace

事前準備

首先必須先安裝 Windows Phone SDK,安裝方式請直接到微軟的官網下載,並隨著步驟安裝,這邊就不特別一步一步講了。(沒特別需求就下一步到底就對了)

http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=13890

 

假設已經安裝好了,那麼…

打開 Microsoft Expression Blend 4

image

 

然後新建一個專案

image

 

我們選擇 Windows Phone Application 將專案名稱取名叫 WOW

image

 

打開後會長這樣

image

 

先將改改標題,讓應用程式有名有份

image

 

接著點選左上方的 Assets

image

 

Control –> All –> Image 找到 Image

image

 

將它拖曳到你的主畫面上,可以看到左下方多了一個元素,畫面上也多了一個方框

image

 

接著我們在畫面上點選方框,在右方的 Source 選張圖片給它

image

 

然後回到左上角的 Behaviors ,找到 PlaySoundAction 以及 RemoveElementAction

image

 

將 PlaySoundAction 以及 RemoveElementAction 拖曳到主畫面的圖片上,就會看到左下角的 Image 底下多了剛剛幫它添加的 Behaviors

image

 

點選 PlaySoundAction ,到右方進行編輯,選個適合的 mp3 給他,記得將音量調到1,避免以為沒有聲音。(這邊可以提供我個人錄製的 wow,沒有版權問題喔!)

image

 

接著存檔,然後 Run Project (其實按下 F5 也可以)

image

 

然後就可以把它點破,並發出你選擇的聲音檔。(剩下就自己多複製幾個泡泡,然後擺到你想要的位置囉!)

SNAGHTMLcdc0b3