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灯, 水草

请输入看板名称,例如:BabyMother站内搜寻

TOP