[軟體] 淺談測試驅動開發 Test-Driven Development ( I ) 對軟體測試的刻板印象

軟體開發是件很奇妙的事情,一直以來我並未刻意使用任何 方法進行軟體開發,說穿了就是 free style,沒有任何 戰術策略,直到最近找了許多 TDD(Test-Driven Development)的相關資料,才驚覺我對 TDD 其實有很深的誤會

對軟體測試的刻板印象

我是真男人,不需要寫測試

許多程序猿(包含我在內),對於自己寫出的code有一定的自信,但 人非聖賢孰能無過? 你不能保證在疲勞的狀態下也不會出錯(事實上任何狀態下都無法保證)。

然而,當你的code悄悄長出蟲(bug)時,只好摸摸鼻子修正,但其實你也沒有把握這次修正一定完美。

其實你是有測試的(以App開發為例),步驟如下:

  1. 確認錯誤區塊
  2. 修正程式碼
  3. 執行App到該區塊
  4. 確認是否修正 <= 測試

通常你會重複2~4,直到你認為錯誤已被修正

不過你只做了一項測試(你有執行到的部份測試),但你要如何保證這次的修改沒有產生其他的bug?

更精確一點的說法是,如何提早知道其他程式被你此次修改給破壞(borken)導致產生其他bug?

如果你沒辦法保證又怎麼稱得上真男人?

寫測試會花費額外的時間

事實上,不寫測試不一定會比較省時間,上一段提到的步驟3是花費的時間成本是難以忽視的,如果你出錯的部分在App的第1頁,那恭喜你,你很快的就可以走到步驟4,但如果錯誤是在第10頁呢?

此外,你也不能保證code強健性(robust),心理會帶著不確定感把code送出。

最後才寫測試

我一直以為測試是軟體開發接近尾聲時才做的事情,當你把程式全部寫完後,開始針對每個物件(class)及元件(component)做測試,確定軟體正常運作的最後檢查,但這樣的想法只對了一半。

的確,我們在發佈(Release)之前,應該經過 QA(Quality Assurance) 的檢測,確保軟體品質達到一定標準,但QA所能做的事情不外乎是 Web 的 A/B Testing 、 App 的 Monkey test 以及直接的人工功能性測試,當然也有強者QA是會寫code的,他可以做程式化及自動化的測試甚至是單元測試(Unit Test),但強者QA不是到處都有,而願意下重金請強者QA的公司在台灣真的是少之又少。

站在公司角度,讓RD寫測試非常不合成本效益,它既沒辦法開發新的功能,也只不過是降低系統出錯機率; 而站在RD的角度,除了會覺得寫測試枯燥乏味以外,後寫測試有一種自我否定的味道,任誰都沒辦法輕易接受。

在這樣的情境下,最後都會變成,不用寫單元測試,直接進到人工測試。

這段不代表最後才寫測試有什麼不對,只是效益沒有比先寫來的好(後面會解釋)。

一個人寫測試,另一個人實作

每個人都有盲點,藉由 Programmer 相互測試可以提昇程式碼的強健度(robust),但副作用是要花大量時間。

畢竟每個人寫程式的方法不盡相同,寫測試的人必須先弄懂別人負責項目的需求,再看懂他寫的物件,才能順利的寫出好的測試,這個方法很好,但有時候你沒有資源與時間讓你這樣做。

先寫完全部測試才開始實作?

既然不要後寫測試,那我全部先寫總行了吧?

除非你有強健的SA(系統分析師)與SD(系統設計師),否則你沒辦法在軟體開發時 按圖施工,但大多時候需求變更是不會停止的,若你寫完全部Test,上面才跟你說設計改了,相信你會不由得說出那 國際問候語

此外,當你先把全部Test寫完的時候,全部一片紅通通,就比一次丟1000題的考卷給你,即便你知道時間還很多,但士氣都已經去掉一大半了。

寫測試是為了抓bug?

Notice that the purpose of testing is to show taht the product works, not discover bugs.
--Graham Lee

測試的目的是為了顯示產品是可以運行的,而不是找到bug。

The test define the acceptance criteria of the product.
Unless all teh tests pass, the code is not good enough.
--Graham Lee

測試定義了「驗收標準」,除非通過全部測試,否則code都不夠好。

如果你沒寫測試,你要如何證明這個物件正確的寫完了?
-- Liyao Chen

在沒寫測試的情況下,如何確保這個物件已經完成當下你賦予它的任務? 你又如何證明它按照需求運作?

在沒寫測試的情況下,你是如何定義這個物件已經寫完了?

我絕對不會害別人的code壞掉

在前一陣子公司內討論使用git的時候,我很自以為是的說了一句話:「本來就不應該commit任何有問題的code。」然而當時的我並 不能確認每次修改的code不會影響到其他人的功能? (不只是沒改別人code而已,共用的model部分也應該正常運作)

當時我並沒實行 TDD,也沒有做Unit Test,我只能確定在我執行的時候(的部份) 看起來是正常的,並沒有 證據可以幫助我確認 這次修改不會影響其他code功能這件事。

是的,我無法確認我的修改是安全的,除非我把全部程式執行一遍,但那成本太高了,所以當時的我只是個信口開河的騙子

結論

而我的理解是,進行TDD 並不是寫測試(test),而是衡量(measure),寫程式的過程中,透過不斷確認自己的所在之處,並且立即修正,才能 更快速穩定的達到目的地

拿唱歌來舉個例子,free style 的方式是把整首歌唱完之後錄下來給別人聽, 每一次都重唱一整首歌,直到最滿意的那次; 而TDD的方式則是一句一句的錄,偷過快速調整,把小區塊拼湊起來,最後合併為一首歌。

你覺得誰錄的比較快呢? 誰又會比較好呢?

Cost of Fixing Bugs Found at Different Stages of the Software Development Process
上表出自Test-Driven iOS Development --Graham Lee

由表1-1可以看出,bug越早被發現時,修復成本越低,只要是能及早發現,就能及早治療。

然而,TDD只是其中幫助你的 策略或是 戰術,如果你是free style信仰者,你也可以把球交給KOBE就好,剩下的交給自由發揮

當然,這篇不是要告訴你TDD就是萬靈藥,每一個軟體開發的情境 不盡相同,應該要看你手上有什麼 資源來決定最佳的方式。

但在你決定要不要用某個策略之前,你應該要先徹底了解一番,分析利弊後在選擇你要的戰術,在對的時間點發揮最大效益。

以上是個人小小淺見,如果有誤請大家多多指教。

Reference

mov轉mp4 (mac)

原來mac可以直接把mov轉成mp4檔案,請見下列步驟:

  1. 在要轉檔的檔案上按[右鍵] -> [服務] -> [為所選的視訊檔案編碼]

  2. 選擇720p的規格

  3. 直接將 .m4v 的副檔名改為 .mp4

避免每次開機都要ssh-add

用多把key來連不同的server (github, gitlab ...etc)
有鑑於每次都忘記的情況下自己記錄一下,透過這個指令可以把privateKey加入keychain。

# Store passphrases in your keychain.
ssh-add -K <% privateKey %>

執行Shell Script打包.ipa檔案

最近學習用Jenkins把iOS開發整個串起來,奇怪的是Jenkins的XCode plugin一直沒辦法打包.ipa檔案,所以就改用執行Shell Script的方式來做。

目標是只用最少檔案產生.ipa檔案

  1. source code
  2. adhoc.mobileprovision
  3. adhoc.sh

原理

因為.mobileprovision檔案裡面有完整的描述,包含所有Build & Archive會用到的參數

  1. PROFILE_NAME
  2. UUID
  3. CODE_SIGN_IDENTITY

檔案配置

因為下列原因,所以把.mobileprovision與build script跟source code放在一起

1. Bundle id與帳號轉換

為了未來置換.mobileprovision檔案(從企業版帳號換到一般帳號)

2. 測試Build script功能

要先確定在local端執行script可以正確的Build出能安裝的.ipa檔案

遇到的問題

1. Build過但是不能安裝

之前有試著把CODE_SIGN_IDENTITY這個參數拿掉,我原本以為Xcodebuild會聰明的拿.mobileprovision內的資料自動去比對,因為他Build沒出錯但最後的.ipa卻不能安裝。

2. 從.mobileprovision抓取CODE_SIGN_IDENTITY

因為這個是一個base64的data,所以在抓取過程中遇到許多挫折,後來找到一個gist卻因為看不動複雜的linux指定而放棄,後來我試著改用application-identifier作為CODE_SIGN_IDENTITY竟然成功了!

剩下就是Build script了~

adhoc.sh

#!/bin/bash

# 使用者參數
PROJECT_NAME="jenkinsdemo"
PROFILE="adhoc.mobileprovision"

function NSLog(){
    clear
    MESSAGE=$1
    echo "$(tput setaf 220)\n ${MESSAGE} \n$(tput sgr 0)"
}


## 確認參數
NSLog "** CHECK PARAMETERS **"

# 沒給ProjectName則用FolderName
if [[(-z "$PROJECT_NAME")]]
then
    PROJECT_NAME=${PWD##*/}
fi

# 沒給ProvisioningProfile從檔案拿
if [[(-z "${PROFILE}")]]
then
    echo "\n$(tput setaf 1)  缺少ProvisioningProfile\n"
    exit 0
fi

## 系統參數
BUILD_PATH="JenkinsBuild"
IPA_FILE="${PROJECT_NAME}.ipa"
ARCHIVE_FILE="${PROJECT_NAME}.xcarchive"

# Error Log檔案
BUILD_ERROR_FILE=build_errors.log

ARCHIVE_PATH="./${BUILD_PATH}/${ARCHIVE_FILE}"
EXPORT_PATH="./${BUILD_PATH}/${IPA_FILE}"

# Provisioning Profile
PROFILE_DATA=$(security cms -D -i ${PROFILE})
PROVISIONING_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c 'Print :Name' /dev/stdin <<< $PROFILE_DATA)
UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $PROFILE_DATA)
APP_ID_PREFIX=$(/usr/libexec/PlistBuddy -c 'Print :ApplicationIdentifierPrefix:0' /dev/stdin <<< $PROFILE_DATA)
CODE_SIGN_IDENTITY=$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' /dev/stdin <<< $PROFILE_DATA)

# Copy 來源的 Provisioning Profile 至 OS
cp -rf $PROFILE ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision

echo "PROJECT_NAME: ${PROJECT_NAME}"
echo "PROVISIONING_PROFILE: ${PROFILE}"
echo "PROFILE_NAME: ${PROVISIONING_PROFILE_NAME}"
echo "UUID: ${UUID}"
echo "CODE_SIGN_IDENTITY: ${CODE_SIGN_IDENTITY}"
echo "APP_ID_PREFIX: ${APP_ID_PREFIX}"
echo ""


## 建置前清除 
NSLog "** CLEAN BEFORE BUILD **"

xcodebuild -alltargets clean
rm -rf ${BUILD_PATH}
rm $BUILD_ERROR_FILE
mkdir ${BUILD_PATH}

## 建置與封裝
NSLog "** BUILD & ARCHIVE **"
# 可換成xcodebuild
xcodebuild \
 -scheme $PROJECT_NAME \
 archive \
 PROVISIONING_PROFILE="$UUID" \
 CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
 -archivePath ${ARCHIVE_PATH} \
 CONFIGURATION_BUILD_DIR=${BUILD_PATH} \
 2>${BUILD_ERROR_FILE} \


## 檢查建置錯誤
NSLog "** CHECK BUILD ERROR ** "

errors=`grep -wc "The following build commands failed" ${BUILD_ERROR_FILE}`
if [ "$errors" != "0" ]
then
    echo "$(tput setaf 1)BUILD FAILED. Error Log:"
    cat ${BUILD_ERROR_FILE}
    echo "$(tput sgr 0)"
    exit 0
else
    rm $BUILD_ERROR_FILE
fi

# ... continue


## 輸出IPA檔案
NSLog "** EXPORT IPA FILE **"

xcodebuild \
 -exportArchive \
 -exportFormat IPA \
 -exportProvisioningProfile $PROVISIONING_PROFILE_NAME \
 -archivePath ${ARCHIVE_PATH} \
 -exportPath ${EXPORT_PATH} \


##移除多餘檔案 
NSLog "** REMOVE UNUSED FILES **"

rm "./${BUILD_PATH}/${PROJECT_NAME}.app"
rm -rf "./${BUILD_PATH}/${PROJECT_NAME}.app.dSYM"
rm -rf ${ARCHIVE_PATH}

# ... finish all

[心得] AngularJS 第七次小聚

距離上回參加AngularJS已經是好幾個月之前的事情了, 其中的每一次都沒搶到票不然就是錯過搶票時間...

會接觸 AngularJS 是因為它的 data binding(.NET 控?) 覺得很直觀所以跳坑,再這之前我只有一些些js基礎,寫過一些jQuery玩玩PhoneGap等等。

回歸正題,這次講題另我印象最深的是

過去我們使用jQuery的時候這類Framework的時候,會把HTML當成是Docuemnt認為它應該乾乾淨淨就好。但在AngularJS的世界裡把HTML定義為UI - @clonn

jQuery的人都知道,在那原生js要寫的落落長+瀏覽器大混戰(js支援語法不同)時代,jQuery的出現讓我們得以用簡潔的語法就去取得、操作DOM,並且在底層就解決瀏覽器在js上的語言隔閡,在其他概念成形之前我們就以為,對,js就應該這樣用,Web就應該這樣寫。

但就我自己寫jQuery的經驗,在DOM的操作上,我常常看不懂我自己之前寫的Code,一下append,一下子又prepend

function displayFood(food){
    var name = food.name;
    var desc = food.description;
    var url = food.url;
    var imageUrl = food.imageUrl;
    var listDiv = $('#listDiv');

    var foodA = $('<a/>').attr('href',url).text(name);
    var foodLi = $('<li/>').append(foodA);
    foodLi.append(' - ' + desc);

    var foodImage = $('<img>').attr('src',imageUrl).attr('width',100).attr('height',100);
    foodLi.append(foodImage);

    $('#listUl').append(foodLi);
    
    $(listDiv)[0].scrollTop = $(listDiv)[0].scrollHeight;
};

不要說交接給別人了,交接給我自己都有問題了...,而這就呼應到宇庭的講題

Code而是寫給人看的,不是寫給機器看的。 - yuting

而同樣的事情Do it Angular's way:

<div id="messagesDiv">
    <ul ng-repeat="food in foods">
        <li>
            <img src={{food.imageUrl}} width='50' height='50'></img>
            <span><a href={{food.url}}>{{food.name}}</a></span>
            <span>: {{food.description}}</span>
            <button ng-click="writeId(item)">X</button>{{food.$id}}<hr>
        </li>
    </ul>
</div>

先不論效能或其他的議題,這才是人看的懂的Code對吧?
jQuery相比就是簡直是文言文 vs. 白話文

以上就是個人小小心得提供給大家參考:)

感謝Blue大大主辦AngularJS小聚,讓我受益良多阿:)

如何簡化使用者介面?

終於被拉近去新產品 wireframe 討論,其實整個計畫前期RD就應該參與討論,尤其是在規劃 wirframe 的人沒有 App 相關設計經驗的時候,那真是互相浪費時間...

也就是因為最近在討論產品的UI所以把這篇轉載過來與大家討論分享:

好的使用者介面

在討論簡化使用者介面之前先想想怎樣的使用者介面是好的

很難回答對吧? 沒關係,就像愛迪生在找讓電燈發光的材料的時候一樣,不知道什麼材料可以發光,但是可以透過知道什麼材料不可以發光而逼近答案。

所以換個方式來問怎樣的使用者介面是不好的

答案有好多,像是太複雜的畫面,太多莫名其妙的功能擺在一起...等等,大多的答案都指向同一個方向Simple is good

如何簡化使用者介面?

最小化你要解決的問題,它越小就越精確的命中使用者的需求。

我會從簡化問題開始。有時候我覺得UI已經不能更簡單了,但還是覺得它很複雜。這時候就是我們重新思考釐清產品目標,使其精鍊。 - Terry Wang, UX designer at Amazon

了解使用者真正的需求才能最簡化。 - Julie Meridian, Senior Experience Designer at LinkedIn

Dan Saffer 提出了下列重點:

  • 移除功能(Remove features) - 越多功能越複雜,使用者必須要多花力氣去學習多餘的功能,更糟糕的情況是這些功能他幾乎沒有用過。

  • 隱藏功能(Hide features) - 用 menu,tabs,dropdowns ... etc. 去隱藏功能,直到它會被用到的時候才顯示。

  • 組織功能(Organize features) - 相似的功能要擺在一起。

  • 站在使用者角度去設計產品模型(Tightly align the user's mental model with the product's conceptual model) - 你越理解使用者的需求,就能設計出越簡單的產品。

  • 讓每個選擇都能看的到(Make every choice visible) - 盡可能顯示所有選項,不要藏在下拉清單裡面,但選項太多的時候適時隱藏一些。

  • 縮減選項(Reduce choice) - 把選項數量縮減到最常使用的項目。

  • 智慧預設值(Smart defaults) - 給予預設值或是提醒,不要讓畫面空空的。

  • 捷徑(Shortcuts) - 對於一些常使用的動作要設捷徑。

  • 不同的平台有不同的流程(Distribute functionality to the right platform) - 根據平台特性提供不同的解決方案,像是裝置、桌上電腦、Web版。不要把不同平台上的使用者介面直接移植。

總結

科技始終來自於惰性,降低使用者用大腦思考的力氣,接受度就越高。

原文來自Quora上的一個討論串What is the best process for simplifying a user interface and experience?

顯示物件詳細資訊

用 NSLog 把物件給印出來只會得到記憶體位置:

2013-11-21 15:27:06.323 GraceGift[19972:70b] <GGUser: 0x8e805c0>

像 java 的 toString 一樣,Obj-C 也有可以轉成字串顯示的 method

- (NSString *)description
{
    NSString *desc = @"format your information you want";
  return desc
}

- (NSString *)debugDescription
{
    NSString *desc = @"format your information you want";
  return desc
}