kyle shanks
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- title: 'Fake 與 Stub & Mock' disqus: kyleAlien --- Fake 與 Stub & Mock === ## OverView of Content [TOC] ## 測試遇到的問題 ### 外部依賴 * **外部依賴**:一般我們在類與類之間會有一個依賴,讓每個類有獨立功能(用來互動),而常常我們的依賴會使用具體物件依賴; **如果這個具體物件是不可控制的,那就無法進行測試** ```kotlin= // External dependency 範例 class Weather { fun isSunny() : Boolean { TODO("網路請求...") // 網路請求,其結果不穩定,無法測試 } } // 依賴 Weather class CampingDependencyWeather constructor(private val weather: Weather) { fun canBookingPlace() : Boolean { if (!weather.isSunny()) { return false } return true } } ``` :::warning 從上面我們可以找到在測試目標(Camping)中的依賴(Weather),我們可以稱這裡為界面(並非 interface 的意思) ::: ### Seam 間隙 & Refactor 重構 * 首先我們先來了解何謂 Seam 間隙 & Refactor 重構 | 詞語 | 定義 | | - | - | | Seam | 是指程式碼可以 **抽換功能的地方** (遵循了 OOP 六大規則中的 "依賴倒置")| | Refactor | 不改變程式碼(函數原有功能,類原本的責任)功能的前提下進行優化,其中包括了可讀性重構,結構重構... 等等 | :::danger * **重構先決條件** 在進行重構之前,請先務必確保你的程式有自動測試的保護,或是你能夠 **清楚的知道這段程式的 ++所有++ 需求結果**! 這樣你才能確保你重構出的程式能夠嵌入舊程式而沒有問題 ::: * **Seam** 縫隙:測試中要解除對外部的不穩定因素依賴,就必須 ^1.^ **先發現外部依賴然後**,然後再 ^2.^ **製作 Seam**,透過這個 Seam 來達成假物件的替換 > 當然這 **假物件也有分類,下面會在提及** ```kotlin= // 2. 製作 Seam (準備重構) class Weather { fun isSunny() : Boolean { TODO("網路請求...") } } // 1. 外部依賴 Weather 實體類 class CampingDependencyWeather constructor(private val weather: Weather) { fun canBookingPlace() : Boolean { if (!weather.isSunny()) { return false } return true } } ``` * **Refactor**:重構分為兩個重點步驟 1. **整個外部依賴類的改變**:將原先的具體類抽換成接口(interface)、委託代理(delegates) * 擷取接口,替換底層實做 ```kotlin= interface IWeather { fun isSunny() : Boolean } class RefactorWeather : IWeather { override fun isSunny() : Boolean { TODO("網路請求...") } } ``` 2. **將原本相依的實體類,替換為依賴抽象**:將偽 interface、delegates 注入實做類 > Fake & Stub 差異下面會說明 * 在測試中注入 Stub(虛設常式)物件 * 屬性注入 Fake 物件 * 方法注入 Fake 物件 * 建構函數中注入 Fake 物件 ```kotlin= // 建構函數注入 class ReactorCampingDependencyWeather constructor( // 依賴抽象 private val weather: IWeather) { fun canBookingPlace() : Boolean { // 使用假物件 if (!weather.isSunny()) { return false } return true } } ``` 以下測試方案使用 Stub `IWeather` 接口 ```kotlin= @Test fun canBookingPlace_notSunny_false() { val stubNotSunny = object : IWeather { override fun isSunny(): Boolean { return false } } assertFalse { ReactorCampingDependencyWeather(stubNotSunny).canBookingPlace() } } ``` ## 假物件 - 分類 在做單元測試時,我們總會需要有一些假設狀況,為了這些假設而製造出來的物件,我們就可以稱其為假物件,分清這些假物件可以讓我們以後更輕鬆的一看名稱就知道其功能 > 建構在相同的認知下去寫測試會更快速、更好維護 其中假物件又可分為下表 | 假物件分類 | 責任 | | -------- | -------- | | Stub(虛設常式) | 模擬當前測試時,所需的假設狀況,並且該狀況是不可變動的(這樣才符合常式) | | Mock(模擬物件) | 驗證當前測試時,與物件的互動性 | | Fake (廣義假物件) | 可操控的假物件 (它可被當作 `Stub` or `Mock` 物件) | :::info * 名詞補充,SUT (System under test): SUT 就是被測試的物件,也有人稱 CUT (Class under test) ::: ### 虛設常式 Stub * **虛設常式 Stub**:就是描述了一個假的情況,並請該物件 **在任何狀況下都不允許更改其內部設置**(狀況需保持不變,才能確保測試的正確性) ```kotlin= interface ICost { fun getCost() : Int } interface IWeather { fun isSunny() : Boolean } class Camping { enum class Type { NON, INNER_TENT, BIG_TENT, NORMAL_TENT, } // 待測試函數 fun getBookingTentType(cost : ICost, weather : IWeather) : Type { val userCost = cost.getCost() val isSunny = weather.isSunny() if(!isSunny) { return Type.INNER_TENT } return when(userCost) { in 1000..2000 -> Type.NORMAL_TENT in 2001 .. Int.MAX_VALUE -> Type.BIG_TENT else -> Type.NON } } } ``` * 從上面程式中我們可以觀察到在呼叫 `getBookingTentType` 函數時,需要帶入兩個接口,這個兩接口就可以做 Stub (照測試情況假設) ```kotlin= // 測試非 isSunny 回傳 false 應該要有的回復情況 @Test fun test_getBookingTentType_NotSunnyDay() { // stub 物件 val stubWeather = object : IWeather { override fun isSunny(): Boolean { return false // 固定返回 } } // stub 物件 val stubCost = object : ICost { override fun getCost(): Int { return 1000 // 固定返回 } } // 待測物件 val sut = Camping() assertEquals(Camping.Type.INNER_TENT, sut.getBookingTentType(stubCost, stubWeather)) } ``` 測試程式與 SUT 與 Stub 假物件的關係如下 > ![](https://i.imgur.com/D3dpn8b.png) :::warning * **要搞清楚測試目的,才能做正確假設** ! ::: ### 模擬物件 Mock * **模擬物件 Mock**:用來驗證目標物件 (SUT),與其相依物件 (Interface) 之間的互動情況;這種情況是你可能需要驗證 相依物件 是否會被呼叫,或是傳入 相依物件 的參數... 等等情況 ```kotlin= interface ICost { fun getCost() : Int } interface IWeather { fun isSunny() : Boolean } interface IEmail { fun sendEmail(type : Camping.Type) } class Camping { enum class Type { NON, INNER_TENT, BIG_TENT, NORMAL_TENT, } fun getBookingTentTypeWithMail(cost : ICost, weather : IWeather, email : IEmail) : Type { val userCost = cost.getCost() val isSunny = weather.isSunny() val result: Type = if(!isSunny) { Type.INNER_TENT } else { when(userCost) { in 1000..2000 -> Type.NORMAL_TENT in 2001 .. Int.MAX_VALUE -> Type.BIG_TENT else -> Type.NON } } // 通知使用者 email.sendEmail(type = result) return result } } ``` 這裡我們驗證 IEmail#`sendEmail` 函數是否收到正確參數;以下我們就來 **手刻** Mock 物件,Mock 物件的重點在於儲存狀態,最終要驗證狀態 ```kotlin= class FakeEmail : IEmail { // 儲存狀態 var getType : Camping.Type? = null override fun sendEmail(type: Camping.Type) { getType = type } } @Test fun test_getBookingTentTypeWithMail() { val exceptType : Camping.Type = Camping.Type.INNER_TENT var getType : Camping.Type? = null // 匿名 Mock 物件 val mockEmail = FakeEmail()} val stubWeather = object : IWeather { override fun isSunny(): Boolean { return false } } val stubCost = object : ICost { override fun getCost(): Int { return 1000 } } val sut = Camping() sut.getBookingTentTypeWithMail(stubCost, stubWeather, mockEmail) // 驗證狀態 assertEquals(exceptType, mockEmail.getType) } ``` > ![](https://i.imgur.com/B3QVBz3.png) 測試程式與 SUT 與 Mock 假物件的關係如下 (忽略 Stub),跟 Stub 的差異是 Mock 是驗證 **互動後的結果 !** > ![](https://i.imgur.com/9MhTkKv.png) :::warning * **要搞清楚測試目的,當前的目的是為了驗證 Mock 的結果** ! * 驗證的 assert 請勿寫在 Mock 物件中 > 未來閱讀程式也不方便知道最初測試的用意、目的 ::: :::danger * 請保持一個測試中只有一個 Mock 物件,因為我們要保持一個測試只測一個結果 ::: ### Stub & Mock 差異 * Stub & Mock 差異如下表 | 假物件類型 | 特點 | 測定目標 | | -------- | -------- | -------- | | Stub | Stub 固定要測試的狀況 | SUT 回傳的結果 | | Mock | 驗證與 Mock 之間的運作 | Mock 回傳的結果 | * Mock 這種互動測試會讓測試情況稍加複雜,如果可以的話盡量使用 Stub 測試;**Mock 代表了一個不確定性,所以一個單元測試只允許存在 ==一個 Mock 假物件==** :::warning * 兩者最根本的差異在於 虛設常式物件 (Stub) 不會讓測試失敗(它就是假設一個情景):測試最終驗證的是 SUT 而不是 Stub 而模擬物件 (Mock) 可以讓測試失敗:測試最終驗證的是 Mock 物件,而不是 SUT ::: ### 廣義假物件 Fake * Fake 可作為 Stub & Mock 所以它有著可變性,所以如果使用 fake 物件請清楚命名為 fake 物件,錯誤的命名可能會導致你對測試的閱讀時間拉長 :::info Fake 在模凌兩可之間,所以使用它要特別 **注意命名** (宣告出的變數的命名) ::: * 同樣用上面的例子 (訂露營區範例:天氣、花費、通知)來建立 fake 物件,並且我們的命名也不詳加定義它是 Stub 還是 Mock ```kotlin= class FakeIWeather constructor(private val isSunnyRes : Boolean): IWeather { override fun isSunny(): Boolean { return isSunnyRes } } class FakeICost constructor(private val costRes : Int): ICost { override fun getCost(): Int { return costRes } } class FakeIEmail : IEmail { private var getType: Camping.Type? = null override fun sendEmail(type: Camping.Type) { getType = type } fun getExpectType(cb: (Camping.Type?) -> Unit) { cb.invoke(getType) } } ``` **最終 Fake 物件是屬於 Stub 還是 Mock 則需要由測試程式來定義!** ```kotlin= @Test fun test_getBookingTentTypeWithMail() { val exceptType : Camping.Type = Camping.Type.INNER_TENT val stubWeather = FakeIWeather(false) val stubCost = FakeICost(1000) val mockEmail = FakeIEmail() // SUT Camping().also { sut -> sut.getBookingTentTypeWithMail(stubCost, stubWeather, mockEmail) } mockEmail.getExpectType { realType -> assertEquals(exceptType, realType) } } ``` > ![](https://i.imgur.com/u1wChw7.png) ## 依賴注入 基於接口的 Seam 有很多種方法可以實做,像是從建構函數注入、方法注入...等等 ### 建構函數注入 ```kotlin= // 待測試程式如下 class ReactorCampingDependencyWeather constructor( private val weather: IWeather) { fun canBookingPlace() : Boolean { if (!weather.isSunny()) { return false } return true } } ``` * 從建構函數注入虛擬常式 Stub,這時就可以發現測試命名相當重要 ```kotlin= // 虛擬常式 Stub class StubNotSunny : IWeather { override fun isSunny(): Boolean { return false } } @Test fun canBookingPlace_notSunny_false() { val stubNotSunny = StubNotSunny() assertFalse { // 建構函數注入 ReactorCampingDependencyWeather(stubNotSunny).canBookingPlace() } } ``` > ![](https://i.imgur.com/myMMyaP.png) * **從建構函數注入時,會衍生出幾個問題、注意點** 1. **建構函數的依賴越多,在測試時就越需要注入更多類別** ```kotlin= // 建構函數隨著時間的更迭會越依賴越多 class DependMultiObjectOnConstruct constructor( private val cost: ICost, private val weather: IWeather, private val email: IEmail) { ... } ``` 這時可以透過兩個方案解決 * 建立一個中間界面,來處理對應的依賴 ```kotlin= // 在測試時只需要做需要的假界面即可 interface DependObj { fun getCost() : ICost fun getWeather() : IWeather fun getEmail() : IEmail } // 依賴中間接口層 class DependMultiObjectOnConstruct2 constructor( private val obj: DependObj) ``` * 使用 IoC(Inversion of Control)容器來達到依賴反轉的功能,透過需求方自己去獲取依賴方的物件來用,就是 IoC 的基本概念 :::warning 以我來說 IoC 如果用不習慣的話,可讀性會變差;應該想想自己的專案是否適合使用,或者思考是否需要修改設計 ::: 2. **何時才是使用建構函數注入的最好時機?** * 建構函數的參數概念就是:對於這個類的必須依賴!如果是非必須依賴則可以改用 setter/getter 來替換注入方案 * 或是你不希望該依賴被外部透過其他方法替換(eg. 不想對外暴露 setter/getter 方法... 等等),也可以在建構函數時注入 * 使用 IoC 套件框架 ### 方法 / 屬性注入 ```kotlin= // 待測試程式如下 class CampingDIMethod { // 屬性注入 lateinit var weather: IWeather // 待測方法 fun canBookingPlace() : Boolean { if (!weather.isSunny()) { return false } return true } } ``` * 透過方法注入可以讓程式更加易讀,也更簡單可以做出測試 (它不會像建構函數的代價這樣大,但是它仍有它必須注意的點) ```kotlin= class NotSunny : IWeather { override fun isSunny(): Boolean { return false } } @Test fun canBookingPlace_DIMethod_False() { val stubNotSunny = NotSunny() val sut = CampingDIMethod().apply { // 呼叫測試目標之前注入 weather = stubNotSunny } assertFalse { // 測試目標 sut.canBookingPlace() } } ``` :::info * 何時才是使用方法 / 屬性注入的最好時機? 使用方法 / 屬性注入時,代表了這是可被替換的物件,它甚至可能不是必須物件,這時就要思考對於設計而言,它是否是 **可以被替換 or 非必須的** ::: > ![](https://i.imgur.com/92dvqrZ.png) * 這裡可以使用一個工廠模式來包裝 IWeather,並讓 SUT 物件依賴工廠,這時我們就可以透過在工廠下創建測試所需的一切狀態,而不用修改到 SUT 物件 以下是一個靜態工廠的測試重構 ```kotlin= // 相依物件使用 CampingCompose 隔開 data class CampingCompose(val weather : IWeather) // 透過設定 Factory 來改變物件特性 object DependencyStaticFactory { private var custom : CampingCompose? = null fun get() : CampingCompose { return custom ?: throw Exception("`setCampingCompose` first") } fun setCampingCompose(compose : CampingCompose?) { custom = compose } } class CampingDependencyFactory { // 依賴靜態工廠所產生出來的元素 private val compose : CampingCompose = DependencyStaticFactory.get() fun canBookingPlace() : Boolean { // 使用靜態工廠所產出的元素 if (!compose.weather.isSunny()) { return false } return true } } ``` :::danger * **注意**: 由於是使用靜態工廠,所以會保留靜態物件,要在測試完畢後立刻釋放物件,否則會導致下一個測試錯誤(拿到上一個測試的假物件) ::: ```kotlin= @AfterEach fun tearDown() { // 釋放測試 DependencyStaticFactory.setCampingCompose(null) } @Test fun canBookingPlace_SunnyDay_True() { val stubWeather = object : IWeather { override fun isSunny(): Boolean { return true } } // 測試前先設定工廠 DependencyStaticFactory.setCampingCompose(CampingCompose(stubWeather)) val sut = CampingDependencyFactory() assertTrue { sut.canBookingPlace() } } ``` > ![](https://i.imgur.com/A7Hejs5.png) ## 偽造深度 我們要知道偽造深度越深,你對測試的程式的控制能力就越強,相對的你的程式會變得越難理解(抽象過重)、並且測試程式會變長;所以這裡我們要討論一下偽造深度 ### 深度一 | 偽造深度 | 說明 | 操作 | e.g | | -------- | - | -------- | -------- | | 1 | 使用建構函數、setter 方式注入假物件 | 保持 SUT 其他成員都是真的,只有一個成員是偽造的 | DI | * 要考慮到添加這個方法、建構函數是否合理?除非有很好的理由,不然建議還是少用(雖然很簡單) ### 深度二 | 偽造深度 | 說明 | 操作 | e.g | | -------- | - | -------- | -------- | | 2 | 製造一個 中間類 並將 SUT 依賴工廠類 | 透過偽造工廠產出的物件來達到 SUT 類不須改變 | 中間類 + 工廠設計 | * 基本上是改變了 SUT 相依物件的關係,透過一層工廠進行隔離,使用起來不難 :::info 但這裡的重點就會變成是誰會使用這個工廠,這個工廠的使用時機點 ::: ### 深度三 | 偽造深度 | 說明 | 操作 | e.g | | -------- | - | -------- | -------- | | 3 | 製造一個 中間類 並將 SUT 依賴抽象工廠類 | 依賴假工廠,也就是連工廠類都可以隨意替換 | 中間類 + 抽象工廠設計 | * 簡單來說:讓 SUT 依賴在一個抽象接口類別,而這個抽象接口又依賴於令一個抽象接口(抽象依賴抽象),相對來講自由度高,不過卻不易理解;可以考量後再使用 ## 其他 ### 測試手段 - Extract & Override * 在上面我們所提出的方案都是發現 Seam、注入假物件、製造中間類、工廠方法... 等等,然而越接近 SUT 本身,我們所需要模擬的物件就相對來說減少 * 控制被測試類(SUT):透過 **繼承 SUT 物件,重寫他的依賴類別**,同樣可以達到注入假物件的功能 ```kotlin= class WeatherManager : IWeather { override fun isSunny(): Boolean { TODO("耗時網路請求") } } // 使用 open 代表可以被繼承 open class CampingExtractOverride { // open 代表可被 Override open fun getWeather(): WeatherManager { return WeatherManager() } fun canBookingPlace() : Boolean { if (!getWeather().isSunny()) { return false } return true } } ``` 測試時只須繼承 `CampingExtractOverride` 並複寫其相依 `getWeather()` 方法即可注入假物件 ```kotlin= class SunnyDay : IWeather { override fun isSunny(): Boolean { return true } } class CampingWithStubWeather : CampingExtractOverride() { override fun getWeather(): IWeather { return SunnyDay() } } @Test fun canBookingPlace_SunnyDay_True() { val sut = CampingWithStubWeather() assertTrue { sut.canBookingPlace() } } ``` > ![](https://i.imgur.com/PufuRzz.png) ### 封裝問題 * 往往學過程式設計者會關心這個類別是否為了 DI 暴露了更多的細節,亦或是該類別是否繼承可被繼承、私有類(方法)不可被繼承... 等等問題 :::success * **物件導向的原則**: **為了限制 API (類別) 的最終使用者的行為**,避免被誤用 * **過度保護設計**: 不許修改、私有建構函數(方法)、不可繼承、不可複寫... 等等都是過度保護設計的特徵 ::: ### 鏈式呼叫的牽扯 * 以往我們在設計程式時,常常會碰到鏈式呼叫(在程式中很好用);但我們要思考鏈式呼叫在測試中就需要作多個假設的情況,對於測試是不太健康的 ```kotlin= // 典型鏈式調用就是 Builder 模式 open class CPU { var core: Int = 2 var hzG: Int = 200 } class Power { enum class PowerCooling { WATER, FAN, OIL, } var type: PowerCooling = PowerCooling.FAN var w: Int = 300 } class Memory { var count: Int = 1 var sizeG: Int = 1 } class Computer(val cpu: CPU, val power: Power, val memory: Memory) { open class Builder { open fun create() : Computer { return Computer(CPU(), Power(), Memory()) } } } class MyComputer { // 測試目標 fun isNormalComputerCanHold(computer: Computer) : Boolean{ // 鏈式調用 val cpuInfo = Computer.Builder().create().cpu return cpuInfo.hzG > computer.cpu.hzG } } ``` 如果上面程式要進行測試,就必須鏈式模擬 ```kotlin= class FakeCpu(private val hz: Int) : CPU() { override var hzG: Int get() = hz set(value) { throw Exception() } } class FakeBuilder(private val cpu: CPU): Computer.Builder() { override fun create(): Computer { return Computer(cpu, Power(), Memory()) } } @Test fun isComputerCanHold_300HzCannotHold() { // 鏈式模擬 val fakeBuilder = FakeBuilder(FakeCpu(300)) val sut = MyComputer() assertFalse { sut.isNormalComputerCanHold(fakeBuilder.create()) } } ``` > ![](https://i.imgur.com/alsr6l5.png) * 上面範例中的鏈式模擬,其實對於測試不太友好,甚至你可以思考一下是否真的有需要使用鏈式調用,或考慮改用 `Extract & Override` 的方案 ```kotlin= open class Computer(val cpu: CPU, val power: Power, val memory: Memory) { open class Builder { open fun create() : Computer { return Computer(CPU(), Power(), Memory()) } } open fun getCpuHz() : Int{ return cpu.hzG } } ``` 測試就可以更簡單的透過 Override `getCpuHz` 來做一個假數據 ```kotlin= class FakeComputer(private val hz: Int): Computer(CPU(), Power(), Memory()) { // Override 目標函數 override fun getCpuHz(): Int { return hz } } @Test fun isComputerCanHold2_300HzCannotHold() { val fakeComputer = FakeComputer(300) val sut = MyComputer() assertFalse { sut.isNormalComputerCanHold(fakeComputer) } } ``` > ![](https://i.imgur.com/r8Z0WlS.png) ### 手刻物件的問題 * 手刻物件其實最大的問題就是耗時、較少能重複使用,要針對每個測試案例狀況去寫物件,並且該物件如果重複使用的話也會造成不同測試之間的依賴 * By the way, 如果一個模擬物件帶有回傳值,那這個模擬物件就同時有 `Stub` and `Mock` 的特性,這時你要更清楚的知道你要測試的目標倒底是倒底是哪個 ```kotlin= // 模擬物件同時含有兩種特性 interface MyNotify { boolean sendNotifyMsg(str: String) } ``` ## Appendix & FAQ :::info ::: ###### tags: `Test`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully