PLT 板


LINE

看板 PLT  RSS
話說在做單元測試的時候,Mock Object 是很常使用的技巧,可以協助開 發者隔離實際的環境,或以人工方式產生錯誤,以加強測試環境的控制。 [1] Mock Object http://en.wikipedia.org/wiki/Mock_object 簡而言之,Mock Object 的精神就是將非你可以控制的部份獨立出來,設 計成可以隨時以其他的實作取代。 在 Scala 裡面,要在做單元測試時使用 Mock Object 是相當簡單,只要 使用內建的 Trait 就可以很容易的達成 Mock Object 的技巧,而且對於 客戶端的使用者而言,並不會感覺到任何使用上的差異。 在這邊,我們使用上次實作的 GeoService 函式庫來做示範,首先先複習 一下上次的函式庫(我有做一些小修正,不過介面是一樣的)。 ================================================================ package org.maidroid.utils import scala.io._ import scala.xml.XML import scala.xml.Node import scala.xml.Elem import java.net.URLEncoder case class GeoPlacemark (val query: String, val address: String, val accuracy: Int, val longitude: Double, val latitude: Double) { val longitudeE6 = (longitude * 1E6) toInt val latitudeE6 = (latitude * 1E6) toInt } class GeoService (val query: String, val locale: String) { private var statusCode = 0 private var errorMessage = "" val placemarkList: List[GeoPlacemark] = doQuery () def this (query: String) = this (query, "") def error = (statusCode, errorMessage) private def createGeoPlacemark (node: Node) = { val address = (node \ "address").text val accuracy = (node \ "AddressDetails" \ "@Accuracy").text val coordinate = (node \ "Point").text.split (",").toList val List (longitude, latitude, _) = coordinate GeoPlacemark (query, address, accuracy.toInt, longitude.toDouble, latitude.toDouble) } private def loadXML = { val url = "http://maps.google.com/maps/geo?" + "q=%s&output=xml&gl=%s". format(URLEncoder.encode(query), locale) val source = Source.fromURL (url, "utf-8") val iter = for (line <- source.getLines) yield line XML.loadString (iter.mkString) } private def doQuery () = { try { val xml = loadXML this.statusCode = (xml \ "Response" \ "Status" \ "code").text.toInt // code 200 means OK if (statusCode != 200) { throw new Exception ("Query Faild") } val placemark = xml \ "Response" \ "Placemark" val addressList = for (node <- placemark) yield createGeoPlacemark (node) addressList.toList } catch { case e => errorMessage = e.getMessage Nil } } } ================================================================ 上述的程式碼中,我們可以發現所謂『非我們所能控制』的部份,就是連 到 Google Maps 服務取得 XML 文件的 loadXML 這個函式。 這就是我們要把它隔離的程式碼,所以我們先設計一個 Trait,裡面有一 個 loadXML 函式的介面。 ================================================================ trait LoadXML { protected def loadXML: Elem } ================================================================ 在這邊要注意的是,由於這個函式是內部使用,不應該透露給外界,所以 我們將其宣告為 protected,這和 Java 的介面裡面只能有 public 不一 樣。 接著,我們更改我們的 GeoService 的實作,要改的部份只有兩個: * 讓 GeoService mix-in LoadXML * 將 GeoService#loadXML 的部份改成 override protected 完整的程式碼如下: ================================================================ package org.maidroid.utils import scala.io._ import scala.xml.XML import scala.xml.Node import scala.xml.Elem import java.net.URLEncoder trait LoadXML { protected def loadXML: Elem } case class GeoPlacemark (val query: String, val address: String, val accuracy: Int, val longitude: Double, val latitude: Double) { val longitudeE6 = (longitude * 1E6) toInt val latitudeE6 = (latitude * 1E6) toInt } class GeoService (val query: String, val locale: String) extends LoadXML { private var statusCode = 0 private var errorMessage = "" val placemarkList: List[GeoPlacemark] = doQuery () def this (query: String) = this (query, "") def error = (statusCode, errorMessage) private def createGeoPlacemark (node: Node) = { val address = (node \ "address").text val accuracy = (node \ "AddressDetails" \ "@Accuracy").text val coordinate = (node \ "Point").text.split (",").toList val List (longitude, latitude, _) = coordinate GeoPlacemark (query, address, accuracy.toInt, longitude.toDouble, latitude.toDouble) } protected def loadXML = { val url = "http://maps.google.com/maps/geo?" + "q=%s&output=xml&gl=%s". format(URLEncoder.encode(query), locale) val source = Source.fromURL (url, "utf-8") val iter = for (line <- source.getLines) yield line XML.loadString (iter.mkString) } private def doQuery () = { try { val xml = loadXML this.statusCode = (xml \ "Response" \ "Status" \ "code").text.toInt // code 200 means OK if (statusCode != 200) { throw new Exception ("Query Faild") } val placemark = xml \ "Response" \ "Placemark" val addressList = for (node <- placemark) yield createGeoPlacemark (node) addressList.toList } catch { case e => errorMessage = e.getMessage Nil } } } ================================================================ 什麼?結束了?!別懷疑,真的就只有這樣子而已,這正是 Scala 吸引 我的地方,夠方便吧! 在測試開始之前,我們先來做幾個 Mock 來用吧,要做 Mock 很簡單,只 要再宣告繼承 LoadXML 的 Trait 即可。在這次的測試中,我們建造了以 下三個 Mock。 * 正確的 XML 回傳值 * 錯誤的 XML 回傳值 * 執行期間發生連線錯誤的 Exception ================================================================ // 正確的 XML 回傳值 trait MockXML extends LoadXML { override def loadXML = <kml xmlns="http://earth.google.com/kml/2.0"> <Response> <!-- 此處省略,請照抄 Google Maps 傳回值 --> </Response> </kml> } // 錯誤的 XML 格式 trait WrongXML extends LoadXML { override def loadXML = <root /> } // 連線錯誤 Exception trait ExceptionXML extends LoadXML { override def loadXML = throw new java.net.ConnectException } ================================================================ 有了這些 Mock 物件之後,就可以著手來寫單元測試了,在這裡所使用的 是 ScalaTest 這個支援多種單元測試風格的 Framework,這次我們用的 是 FlatSpec 這個 Behavior Driven Development 測試。 [2] ScalaTest http://www.scalatest.org/ 由於使用 FlatSpec 所寫出的測試都相當直覺,看起來就像英文句子,所 以程式碼的部份就不做詳細的解釋了。 在這邊要注意的地方,就是當我們要使用 Mock 的時候,直需要在建立物 件時將要使用的 Mock 給 mix-in 進來就好,範例如下。 ================================================================ // 不使用任何 Mock val noMock = new GeoService ("中央研究院") // 分別使用上述三種 Mock val mock1 = new GeoService ("中央研究院") with MockXML val mock2 = new GeoService ("中央研究院") with WrongXML val mock3 = new GeoService ("中央研究院") with ExceptionXML ================================================================ 知道了上述的規則後,我們就可以使用這三個 Mock 撰寫我們的測試案例 了,完整的測試案例如下。 ================================================================ import org.scalatest.FlatSpec import org.scalatest.matchers.ShouldMatchers import org.maidroid.utils._ class GeoServiceSpec extends FlatSpec with ShouldMatchers { // 不使用任何 Mock 物件 "A GeoService" should "retun a list of GeoPlacemark when successed" in { val service = new GeoService ("中央研究院") val correct = GeoPlacemark ("中央研究院", "115 Taiwan Taipei City Nangang "+ "District中央研究院", 9, 121.6122646, 25.0405918) service.placemarkList should be === List (correct) } // 使用 MockXML 以保確取得正確的 XML 回傳值 it should "has statusCode 200 and no error message when successed" in { val service = new GeoService ("中央研究院") with MockXML service.error should be === (200, "") } // 例用 WrongXML Mock 測試當回傳值為不合格式的 XML 時的狀況 it should "has statusCode 0 and empty list when XML format is wrong" in { val service = new GeoService ("中央研究院") with WrongXML service.placemarkList should be === Nil service.error._1 should be === 0 } // 例用 ExceptionXML 測試發生 Exception 時的行為 it should "has statusCode 0 and empty list when Exception occurred" in { val service = new GeoService ("中央研究院") with ExceptionXML service.placemarkList should be === Nil service.error._1 should be === 0 } } ================================================================ 如何?要在 Scala 使用 Mock Object 進行單元測試很方便吧?!請多多 利用這個技巧,讓單元測試變得更愉快喲! -- ~ 白馬帶著她一步步地回到中原。白馬已經老了,只能慢慢地走, 'v' Brian Hsu 但終是能回到中原的。江南有楊柳、桃花,有燕子、金魚…… // \\ ( 墳 墓 ) /( )\ 但這個美麗的姑娘就像古高昌國人那樣固執。 【白馬嘯西風】 ^`~'^ http://bone.twbbs.org.tw/blog 『那都是很好很好的,可我偏不喜歡。』 --



※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 59.120.199.114
1F:推 godfat:btw, 之前在試 scalacheck, 不過跟 2.8 相容還不完整.. 01/16 16:29
2F:→ godfat:現在不知道如何了?有一陣子沒注意了 01/16 16:29
3F:→ brianhsu:用在 2.8 上用的話好像要抓 Snapshot 版,不過我沒試過。 01/16 18:48
4F:→ brianhsu:http://0rz.tw/h72zm 01/16 18:48







like.gif 您可能會有興趣的文章
icon.png[問題/行為] 貓晚上進房間會不會有憋尿問題
icon.pngRe: [閒聊] 選了錯誤的女孩成為魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一張
icon.png[心得] EMS高領長版毛衣.墨小樓MC1002
icon.png[分享] 丹龍隔熱紙GE55+33+22
icon.png[問題] 清洗洗衣機
icon.png[尋物] 窗台下的空間
icon.png[閒聊] 双極の女神1 木魔爵
icon.png[售車] 新竹 1997 march 1297cc 白色 四門
icon.png[討論] 能從照片感受到攝影者心情嗎
icon.png[狂賀] 賀賀賀賀 賀!島村卯月!總選舉NO.1
icon.png[難過] 羨慕白皮膚的女生
icon.png閱讀文章
icon.png[黑特]
icon.png[問題] SBK S1安裝於安全帽位置
icon.png[分享] 舊woo100絕版開箱!!
icon.pngRe: [無言] 關於小包衛生紙
icon.png[開箱] E5-2683V3 RX480Strix 快睿C1 簡單測試
icon.png[心得] 蒼の海賊龍 地獄 執行者16PT
icon.png[售車] 1999年Virage iO 1.8EXi
icon.png[心得] 挑戰33 LV10 獅子座pt solo
icon.png[閒聊] 手把手教你不被桶之新手主購教學
icon.png[分享] Civic Type R 量產版官方照無預警流出
icon.png[售車] Golf 4 2.0 銀色 自排
icon.png[出售] Graco提籃汽座(有底座)2000元誠可議
icon.png[問題] 請問補牙材質掉了還能再補嗎?(台中半年內
icon.png[問題] 44th 單曲 生寫竟然都給重複的啊啊!
icon.png[心得] 華南紅卡/icash 核卡
icon.png[問題] 拔牙矯正這樣正常嗎
icon.png[贈送] 老莫高業 初業 102年版
icon.png[情報] 三大行動支付 本季掀戰火
icon.png[寶寶] 博客來Amos水蠟筆5/1特價五折
icon.pngRe: [心得] 新鮮人一些面試分享
icon.png[心得] 蒼の海賊龍 地獄 麒麟25PT
icon.pngRe: [閒聊] (君の名は。雷慎入) 君名二創漫畫翻譯
icon.pngRe: [閒聊] OGN中場影片:失蹤人口局 (英文字幕)
icon.png[問題] 台灣大哥大4G訊號差
icon.png[出售] [全國]全新千尋侘草LED燈, 水草

請輸入看板名稱,例如:Tech_Job站內搜尋

TOP