單元測試學習筆記02_虛設常式(stub)、模擬物件(Mock)_call api為例
一般測試中比較容易頭疼就是在於對於外部資源會有依賴
(像是一段function涉及到發信、寫Log檔、讀取外部文件或者call web api, web service等)
而除了外部資源外還可能會有我只想驗證自己負責的class中特定方法但該方法可能又有去呼叫或者存取也就是依賴到其餘class的方法甚至層層相依等問題
如下圖所示
測試過程往往會遇到目標要測試的方法當中又相依很多其餘的class
甚至會往下相依多個層級
此時我們會藉由類似建構假物件(Fake)的方式來把關注點分離
進行隔離測試程式碼
「各人自掃門前雪,莫管他人瓦上霜」的含意。
Microsoft Fakes 會以「虛設常式」或「填充碼」取代應用程式的其他部分,協助您隔離要測試的程式碼。 這些是受測試所控制的一些程式碼片段。 藉由隔離待測的程式碼,您可以在正確的位置尋找測試失敗的原因。 即使應用程式的其他部分還無法運作,您也可以利用虛設常式和填充碼。
該框架鎖定義Fakes 分為兩種類別:
虛設常式(stub)
會以一小段實作相同介面的類別取代類別。 若要使用虛設常式,您所設計的應用程式必須讓每個元件只相依於介面,而不相依於其他元件。 (「元件」表示一起設計及更新的類別或類別群組,通常會包含在組件中)。
填充碼(shim)
會在執行階段修改應用程式的編譯程式碼,以便執行您的測試所提供的填充碼,而不是進行指定的方法呼叫。 您可以使用填充碼取代您無法修改的組件 (例如 .NET 組件) 的呼叫。
不過很遺憾這功能只有在enterprice版本的visual studio才有支援
這裡我就沒特別用它來示範了
而一般Fake通用觀念就是上一篇提及到的
主要分成兩大類
虛設常式(stub)
通常用於驗證目標回傳值,以及驗證目標物件狀態的改變(State-Based Testing)
These are extremely simplified versions of Mocks, which typically just return a set of hard-coded values, and/or fail, again, in simplistic ways.
模擬物件(Mock)
用於驗證目標物件與外部相依介面的互動方式(Interaction-Based Testing)
(使用情境發生在於沒有回傳值的情況)
A full-on stand in for the dependency, that replicates most or all of the behavior that you actually give a s**t about. These can actually get somewhat hairy (hence the “Code Smell” crack above), since, in extremis, you basically need to track/replicate the functionality of the dependency.
這裡我模擬一個call web api 查找出新竹縣長照機構位置資料的需求
這裡copy一份API回傳結果以免後續存取不到的人不好模擬
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 | [ { "編號": "1", "服務類型": "居家服務", "特約鄉鎮": "竹北", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "2", "服務類型": "居家服務", "特約鄉鎮": "竹北", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "3", "服務類型": "居家服務", "特約鄉鎮": "竹北", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "304", "地址": "新竹縣新豐鄉莊敬街26巷1弄15號", "電話": "03-5576556" }, { "編號": "4", "服務類型": "居家服務", "特約鄉鎮": "竹北", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "5", "服務類型": "居家服務", "特約鄉鎮": "新豐", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "6", "服務類型": "居家服務", "特約鄉鎮": "新豐", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "304", "地址": "新竹縣新豐鄉莊敬街26巷1弄15號", "電話": "03-5576556" }, { "編號": "7", "服務類型": "居家服務", "特約鄉鎮": "新豐", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "8", "服務類型": "居家服務", "特約鄉鎮": "新豐", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "9", "服務類型": "居家服務", "特約鄉鎮": "湖口", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "304", "地址": "新竹縣新豐鄉莊敬街26巷1弄15號", "電話": "03-5576556" }, { "編號": "10", "服務類型": "居家服務", "特約鄉鎮": "湖口", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "11", "服務類型": "居家服務", "特約鄉鎮": "湖口", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "12", "服務類型": "居家服務", "特約鄉鎮": "湖口", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "13", "服務類型": "居家服務", "特約鄉鎮": "湖口", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "14", "服務類型": "居家服務", "特約鄉鎮": "寶山", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "15", "服務類型": "居家服務", "特約鄉鎮": "寶山", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "304", "地址": "新竹縣新豐鄉莊敬街26巷1弄15號", "電話": "03-5576556" }, { "編號": "16", "服務類型": "居家服務", "特約鄉鎮": "寶山", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "17", "服務類型": "居家服務", "特約鄉鎮": "寶山", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "18", "服務類型": "居家服務", "特約鄉鎮": "新埔", "機構名稱": "新竹縣私立禾意關懷協會居家式服務類長期照顧服務機構", "郵遞區號": "305", "地址": "新竹縣新埔鎮四座里楊新路一段5號", "電話": "03-5886158" }, { "編號": "19", "服務類型": "居家服務", "特約鄉鎮": "新埔", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "20", "服務類型": "居家服務", "特約鄉鎮": "新埔", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立長安老人養護中心", "郵遞區號": "310", "地址": "新竹縣竹東鎮長安路130號", "電話": "03-5943987" }, { "編號": "21", "服務類型": "居家服務", "特約鄉鎮": "新埔", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "22", "服務類型": "居家服務", "特約鄉鎮": "新埔", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "23", "服務類型": "居家服務", "特約鄉鎮": "關西", "機構名稱": "新竹縣私立禾意關懷協會居家式服務類長期照顧服務機構", "郵遞區號": "305", "地址": "新竹縣新埔鎮四座里楊新路一段5號", "電話": "03-5886158" }, { "編號": "24", "服務類型": "居家服務", "特約鄉鎮": "關西", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "25", "服務類型": "居家服務", "特約鄉鎮": "關西", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立長安老人養護中心", "郵遞區號": "310", "地址": "新竹縣竹東鎮長安路130號", "電話": "03-5943987" }, { "編號": "26", "服務類型": "居家服務", "特約鄉鎮": "關西", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "27", "服務類型": "居家服務", "特約鄉鎮": "關西", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "28", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立長安老人養護中心", "郵遞區號": "310", "地址": "新竹縣竹東鎮長安路130號", "電話": "03-5943987" }, { "編號": "29", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "30", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "社團法人中華民國原住民老人長期照顧暨婦幼受暴緊急安置發展關懷協會附設新竹縣私立拿互依居家長照機構", "郵遞區號": "313", "地址": "新竹縣尖石鄉嘉樂村2鄰麥樹仁5號", "電話": "03-5841968" }, { "編號": "31", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮幸福路121巷2號9樓", "電話": "03-5960775" }, { "編號": "32", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "33", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "34", "服務類型": "居家服務", "特約鄉鎮": "竹東", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "35", "服務類型": "居家服務", "特約鄉鎮": "芎林", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立長安老人養護中心", "郵遞區號": "310", "地址": "新竹縣竹東鎮長安路130號", "電話": "03-5943987" }, { "編號": "36", "服務類型": "居家服務", "特約鄉鎮": "芎林", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "37", "服務類型": "居家服務", "特約鄉鎮": "芎林", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "38", "服務類型": "居家服務", "特約鄉鎮": "芎林", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市四維街228號", "電話": "03-5536679" }, { "編號": "39", "服務類型": "居家服務", "特約鄉鎮": "芎林", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "40", "服務類型": "居家服務", "特約鄉鎮": "橫山", "機構名稱": "社團法人中華民國原住民老人長期照顧暨婦幼受暴緊急安置發展關懷協會附設新竹縣私立拿互依居家長照機構", "郵遞區號": "313", "地址": "新竹縣尖石鄉嘉樂村2鄰麥樹仁5號", "電話": "03-5841968" }, { "編號": "41", "服務類型": "居家服務", "特約鄉鎮": "橫山", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立長安老人養護中心", "郵遞區號": "310", "地址": "新竹縣竹東鎮長安路130號", "電話": "03-5943987" }, { "編號": "42", "服務類型": "居家服務", "特約鄉鎮": "橫山", "機構名稱": "社團法人中華民國紅十字會台灣省新竹縣支會附設新竹縣私立博愛居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市新泰路92號7樓之1", "電話": "03-5535850" }, { "編號": "43", "服務類型": "居家服務", "特約鄉鎮": "橫山", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "44", "服務類型": "居家服務", "特約鄉鎮": "橫山", "機構名稱": "信馨居服有限公司附設新竹縣私立信馨居家式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "新竹縣竹北市中泰路6號2樓", "電話": "03-5528207" }, { "編號": "45", "服務類型": "居家服務", "特約鄉鎮": "尖石", "機構名稱": "社團法人中華民國原住民老人長期照顧暨婦幼受暴緊急安置發展關懷協會附設新竹縣私立拿互依居家長照機構", "郵遞區號": "313", "地址": "新竹縣尖石鄉嘉樂村2鄰麥樹仁5號", "電話": "03-5841968" }, { "編號": "46", "服務類型": "居家服務", "特約鄉鎮": "尖石", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立長安老人養護中心", "郵遞區號": "310", "地址": "新竹縣竹東鎮長安路130號", "電話": "03-5943987" }, { "編號": "47", "服務類型": "居家服務", "特約鄉鎮": "尖石", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "48", "服務類型": "居家服務", "特約鄉鎮": "五峰", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮幸福路121巷2號9樓", "電話": "03-5960775" }, { "編號": "49", "服務類型": "居家服務", "特約鄉鎮": "五峰", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "50", "服務類型": "居家服務", "特約鄉鎮": "北埔", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮幸福路121巷2號9樓", "電話": "03-5960775" }, { "編號": "51", "服務類型": "居家服務", "特約鄉鎮": "北埔", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "52", "服務類型": "居家服務", "特約鄉鎮": "峨眉", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福居家式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮幸福路121巷2號9樓", "電話": "03-5960775" }, { "編號": "53", "服務類型": "居家服務", "特約鄉鎮": "峨眉", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "新竹縣竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "54", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "財團法人台灣省天主教會新竹教區附設新竹縣私立竹北社區式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "竹北市福興東路1段365號3樓", "電話": "03-5500797分機311" }, { "編號": "55", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "新竹縣私立安歆綜合式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "竹北市四維街228號", "電話": "03-5536679分機11" }, { "編號": "56", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "財團法人新竹縣天主教世光教養院附設新竹縣私立橫山社區式服務類長期照顧服務機構", "郵遞區號": "312", "地址": "橫山鄉新興村新興街129號", "電話": "03-5932667" }, { "編號": "57", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "財團法人新竹縣天主教世光教養院附設新竹縣私立北埔社區式服務類長期照顧服務機構", "郵遞區號": "314", "地址": "北埔鄉中正路30號", "電話": "03-5805586" }, { "編號": "58", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "長泰護理之家", "郵遞區號": "304", "地址": "新豐鄉埔和村埔頂363之10號", "電話": "03-5689333" }, { "編號": "59", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "社團法人中華民國五福社會服務協會附設新竹縣私立五福社區式服務類長期照顧服務機構", "郵遞區號": "303", "地址": "湖口鄉中正村達生五路280號", "電話": "03-5908205" }, { "編號": "60", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "財團法人喜憨兒社會福利基金會新竹分事務所附設新竹縣私立喜歡你社區式服務類長期照顧服務機構", "郵遞區號": "307", "地址": "芎林鄉文山路565號", "電話": "03-5923828" }, { "編號": "61", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "新竹縣私立信馨社區式服務類長期照顧服務機構", "郵遞區號": "306", "地址": "關西鎮中山東路85號", "電話": "03-5870550" }, { "編號": "62", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "臺北榮民總醫院新竹分院附設社區式長期照顧服務機構", "郵遞區號": "310", "地址": "竹東鎮中豐路一段81號", "電話": "03-5962134分機883、882" }, { "編號": "63", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "社團法人中華民國誠馨照顧協會附設新竹縣私立誠馨綜合式服務類長期照顧服務機構", "郵遞區號": "310", "地址": "竹東鎮興農街119號", "電話": "03-5104527" }, { "編號": "64", "服務類型": "日間照顧服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "新竹縣蒲公英關懷弱勢權益促進協會附設新竹縣私立蒲公英社區式服務類長期照顧服務機構", "郵遞區號": "302", "地址": "竹北市新泰路92號2樓", "電話": "03-5551096" }, { "編號": "65", "服務類型": "小規模多機能服務", "特約鄉鎮": "新竹縣全區", "機構名稱": "財團法人新竹縣天主教世光教養院附設新竹縣私立新埔社區式服務類長期照顧服務機構", "郵遞區號": "305", "地址": "新埔鎮五埔里新關路183號", "電話": "03-5882329" }, { "編號": "66", "服務類型": "家庭托顧", "特約鄉鎮": "尖石", "機構名稱": "新竹縣私立嘉樂社區式服務類長期照顧服務機構", "郵遞區號": "313", "地址": "新竹縣尖石鄉嘉樂村麥樹仁5號", "電話": "03-5841968" } ] |
依據資料回應格式準備好DTO(供後續json序列化接收處理)
由於預設api回傳是以中文字定義的Key在程式碼中不可能用中文
這塊可以參照之前JsonProperty部分使用
跟後續序列化需要引入Json.Net套件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1 { class CareInstitution { [JsonProperty(PropertyName = "編號")] public int Id { get; set; } [JsonProperty(PropertyName = "服務類型")] public string ServiceType { get; set; } [JsonProperty(PropertyName = "特約鄉鎮")] public string SpecialContractTown { get; set; } [JsonProperty(PropertyName = "機構名稱")] public string Organization { get; set; } [JsonProperty(PropertyName = "郵遞區號")] public string PostCode { get; set; } [JsonProperty(PropertyName = "地址")] public string Address { get; set; } [JsonProperty(PropertyName = "電話")] public string Phone { get; set; } } } |
呼叫api則透過RestSharp套件來實踐
CareInstitutonWebHelper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using Newtonsoft.Json; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1 { public class CareInstitutonWebHelper { public List<CareInstitution> GetCareInstitutionData() { List<CareInstitution> ApiResult = null; var client = new RestClient("https://ws.hsinchu.gov.tw/001/Upload/1/opendata/8774/283/847b3ad3-6b1e-439f-bb7e-869c53727d35.json"); client.Timeout = -1; var request = new RestRequest(Method.GET); IRestResponse response = client.Execute(request); //Console.WriteLine(response.Content); if (response.StatusCode == System.Net.HttpStatusCode.OK) { ApiResult = JsonConvert.DeserializeObject<List<CareInstitution>>(response.Content); } return ApiResult; } } } |
我們想要有一個透過服務類型或路段地址、郵遞區號、特約地區
可以查找到機構資訊的function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using Newtonsoft.Json; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1 { public class CareInstitutonWebHelper { public CareInstitution GetCareInstitutionData(string address, string post_code, string service_type, string special_contract) { List<CareInstitution> ApiResult = null; var client = new RestClient("https://ws.hsinchu.gov.tw/001/Upload/1/opendata/8774/283/847b3ad3-6b1e-439f-bb7e-869c53727d35.json"); client.Timeout = -1; var request = new RestRequest(Method.GET); IRestResponse response = client.Execute(request); //Console.WriteLine(response.Content); if (response.StatusCode == System.Net.HttpStatusCode.OK) { ApiResult = JsonConvert.DeserializeObject<List<CareInstitution>>(response.Content); } return ApiResult.Where(r => post_code == post_code) .Where(r => r.ServiceType == service_type) .Where(r => r.SpecialContractTown == special_contract) .Where(r => r.Address.Contains(address)) .FirstOrDefault(); } } } |
新增測試專案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using Microsoft.VisualStudio.TestTools.UnitTesting; using UnitTestApp1; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1.Tests { [TestClass()] public class CareInstitutonWebHelper_Tests { [TestMethod()] public void GetCareInstitutionData_Test() { //Arrange var CareApi = new CareInstitutonWebHelper(); var institutions = new List<CareInstitution>(); var service_type = "居家服務"; var address = "竹北市四維街228號"; var post_code = "302"; var special_contract = "新豐"; var expected = "新竹縣私立安歆綜合式服務類長期照顧服務機構"; //Act var actual = CareApi.GetCareInstitutionData(address, post_code, service_type, special_contract); //Assert Assert.AreEqual(expected, actual.Organization); } } } |
目前直Call api的寫法會導致外部依賴
假如網路斷線或者對方API還沒更新完暫時關閉就無法RUN這塊測試
而這都不屬於我們程式開發可控範疇
因此需要把外部行為隔離出去,想方式去模擬而非真正地去call api。
這裡將Call Api抽離出來額外一個method 重構
並把修飾改為protected virtual
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | using Newtonsoft.Json; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1 { public class CareInstitutonWebHelper { public CareInstitution GetCareInstitutionData(string address, string post_code, string service_type, string special_contract) { List<CareInstitution> ApiResult = null; ApiResult = CallApi(ApiResult); return ApiResult.Where(r => post_code == post_code) .Where(r => r.ServiceType == service_type) .Where(r => r.SpecialContractTown == special_contract) .Where(r => r.Address.Contains(address)) .FirstOrDefault(); } protected virtual List<CareInstitution> CallApi(List<CareInstitution> ApiResult) { var client = new RestClient("https://ws.hsinchu.gov.tw/001/Upload/1/opendata/8774/283/847b3ad3-6b1e-439f-bb7e-869c53727d35.json"); client.Timeout = -1; var request = new RestRequest(Method.GET); IRestResponse response = client.Execute(request); //Console.WriteLine(response.Content); if (response.StatusCode == System.Net.HttpStatusCode.OK) { ApiResult = JsonConvert.DeserializeObject<List<CareInstitution>>(response.Content); } return ApiResult; } } } |
接著要探討到的就是
如何去模擬call api這個行為
虛設常式 Stub
我們要去模擬外部回傳值或是回應
這邊新增一個目錄Stub好利於分類存放
並新增一個class 命名為CareInstitutonWebHelperStub
將其繼承CareInstitutonWebHelper後再對CallApi進行覆寫
改寫為可以從外部加入假資料的模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1; namespace UnitTestApp1_Tests1.Stub { public class CareInstitutonWebHelperStub : CareInstitutonWebHelper { public List<CareInstitution> FakeApiResult; /// <summary> /// 模擬api 回傳 /// </summary> /// <param name="ApiResult"></param> /// <returns></returns> protected override List<CareInstitution> CallApi(List<CareInstitution> ApiResult) { if (FakeApiResult != null) { return FakeApiResult; } return base.CallApi(ApiResult); } } } |
因此若我們外部有預先傳入FakeApiResult的資料就會將該預先準備的假資料回傳
改寫原先測試專案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | using Microsoft.VisualStudio.TestTools.UnitTesting; using UnitTestApp1; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1_Tests1.Stub; using Newtonsoft.Json; namespace UnitTestApp1.Tests { [TestClass()] public class CareInstitutonWebHelper_Tests { [TestMethod()] public void GetCareInstitutionData_Test() { //Arrange Real //var CareApi = new CareInstitutonWebHelper(); //Arrange Fake(Stub) var CareApiStub = new CareInstitutonWebHelperStub(); var institutions = new List<CareInstitution>(); var service_type = "居家服務"; var address = "竹北市四維街228號"; var post_code = "302"; var special_contract = "新豐"; StringBuilder sbApiResult = new StringBuilder(); sbApiResult.Append("["); sbApiResult.Append("{'編號': '1','服務類型': '居家服務','特約鄉鎮': '竹北','機構名稱': '新竹縣私立安歆綜合式服務類長期照顧服務機構','郵遞區號': '302','地址': '新竹縣竹北市四維街228號','電話': '03-5536679'}"); sbApiResult.Append(","); sbApiResult.Append("{'編號': '5','服務類型': '居家服務','特約鄉鎮': '新豐','機構名稱': '新竹縣私立安歆綜合式服務類長期照顧服務機構','郵遞區號': '302','地址': '新竹縣竹北市四維街228號','電話': '03-5536679'}"); sbApiResult.Append("]"); CareApiStub.FakeApiResult = JsonConvert.DeserializeObject<List<CareInstitution>>(sbApiResult.ToString()); var expected = "新竹縣私立安歆綜合式服務類長期照顧服務機構"; //Act Real //var actual = CareApi.GetCareInstitutionData(address, post_code, service_type, special_contract); //Act Fake(Stub) var actual = CareApiStub.GetCareInstitutionData(address, post_code, service_type, special_contract); //Assert Assert.AreEqual(expected, actual.Organization); } } } |
如此
即便斷網或者對方API未提供也能做測試驗證了
只不過通常都會需要先得知對方預期的api 回傳結果較好
(備註:雙引號記得要統一取代為單引號)
所以這邊主要重點在於去將有外部依賴的區塊給抽離
並改為具有可更改覆寫彈性
模擬物件 Mock
這邊我們再用一個臺中交通資訊API
http://e-traffic.taichung.gov.tw/DataAPI/swagger/ui/index#!/TraStationAPI/TraStationAPI_Get
有關於台鐵資訊的API
其Swagger API Document十分詳細
這邊我們取一個簡單的
可以列出全部的
http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI
跟指定特定id的
http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI/7310
新增一個TraStationWebHelper.cs 類別封裝api存取的邏輯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using Newtonsoft.Json; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1 { public class TraStationWebHelper { //取得指定之台鐵車站資訊 public TraStationInfoDTO GetSpecificTraStationInfo(string StationID) { return CallApiResult(StationID); } protected virtual TraStationInfoDTO CallApiResult(string StationID) { TraStationInfoDTO traStationInfoDTO = null; var client = new RestClient($"http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI/{StationID}"); client.Timeout = -1; var request = new RestRequest(Method.GET); IRestResponse response = client.Execute(request); if (response.StatusCode == System.Net.HttpStatusCode.OK) { traStationInfoDTO = JsonConvert.DeserializeObject<TraStationInfoDTO>(response.Content); } return traStationInfoDTO; } } } |
跟相應API回傳JSON要承接反序列化的TraStationInfoDTO.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1 { public class TraStationInfoDTO { public string StationID { get; set; } public string StationName { get; set; } public string StationAddress { get; set; } public string StationPhone { get; set; } } } |
TraStationWebHelperStub.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1; namespace UnitTestApp1_Tests1.Stub { public class TraStationWebHelperStub : TraStationWebHelper { public TraStationInfoDTO FakeApiResult; protected override TraStationInfoDTO CallApiResult(string StationID) { if(FakeApiResult != null) { return FakeApiResult; } return base.CallApiResult(StationID); } } } |
TraStationWebHelper 對應的測試專案(比照前面STUB作法方式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using Microsoft.VisualStudio.TestTools.UnitTesting; using UnitTestApp1; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1_Tests1.Stub; namespace UnitTestApp1.Tests { [TestClass()] public class TraStationWebHelper_Tests { [TestMethod()] public void GetSpecificTraStationInfo_Test() { //Arrange Real //var traStationWebHelper = new TraStationWebHelper(); //Arrange Fake(Stub) var traStationWebHelperSub = new TraStationWebHelperStub(); traStationWebHelperSub.FakeApiResult = new TraStationInfoDTO() { StationID = "7310", StationName = "雙溪", StationAddress = "22744新北市雙溪區新基里朝陽街 1 號", StationPhone = "02-24932980" }; var StationID = "7310"; var expected = "雙溪"; //Act //var actual = traStationWebHelper.GetSpecificTraStationInfo(StationID); //Act Fake(Stub) var actual = traStationWebHelperSub.GetSpecificTraStationInfo(StationID); //Assert Assert.AreEqual(expected, actual.StationName); } } } |
在這案例中傳入的URL其實也算外部依賴也有機會讓測試失敗
在測試階段都會正確但無法得知真正上線運行結果是否也能如預期正常
比方在不設FakeStub且指定的StationID根本不存於列表中
此時可能會跑NULL 錯誤
這邊藉由抽離出抽象層面interface也就是針對call api部分
./WebHelper/IApiHandler.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1.WebHelper { public interface IApiHandler { string Get(string _url); } } |
在自訂一個ApiHandler具體的Class去實作抽象
./WebHelper/ApiHandler.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace UnitTestApp1.WebHelper { public class ApiHandler : IApiHandler { public string Get(string _url) { var client = new RestClient(_url); client.Timeout = -1; var request = new RestRequest(Method.GET); IRestResponse response = client.Execute(request); if (response.StatusCode == System.Net.HttpStatusCode.OK) { return response.Content; } return ""; } } } |
再透過建構子依賴注入到目前helper class當中
使其不依賴具體而是依賴抽象interface
TraStationWebHelper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | using Newtonsoft.Json; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1.WebHelper; namespace UnitTestApp1 { public class TraStationWebHelper { IApiHandler apiHandler; public TraStationWebHelper(IApiHandler apiHandler) { this.apiHandler = apiHandler; } //取得指定之台鐵車站資訊 public TraStationInfoDTO GetSpecificTraStationInfo(string StationID) { return CallApiResult(StationID); } protected virtual TraStationInfoDTO CallApiResult(string StationID) { TraStationInfoDTO traStationInfoDTO = null; string input_url = $"http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI/{StationID}"; var json_result = apiHandler.Get(input_url); if (!string.IsNullOrEmpty(json_result)) { traStationInfoDTO = JsonConvert.DeserializeObject<TraStationInfoDTO>(json_result); } //var client = new RestClient($"http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI/{StationID}"); //client.Timeout = -1; //var request = new RestRequest(Method.GET); //IRestResponse response = client.Execute(request); //if (response.StatusCode == System.Net.HttpStatusCode.OK) //{ // traStationInfoDTO = JsonConvert.DeserializeObject<TraStationInfoDTO>(response.Content); //} return traStationInfoDTO; } } } |
並把之前和Stub有關的程式都先註解掉不然會開始有錯誤
再來要換成Mock方式來練習
新增自訂的Mock
TraStationWebHelperMock並去實作IApiHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1.WebHelper; namespace UnitTestApp1_Tests1.Mock { public class TraStationWebHelperMock : IApiHandler { public string Get(string _url) { if (_url == "http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI/7310") { var json_result = "{'StationID':'7310','StationName':'雙溪','StationAddress':'22744新北市雙溪區新基里朝陽街 1 號','StationPhone':'02-24932980'}"; return json_result; } return ""; } } } |
在Mock中我們去撰寫一個行為利於我們
驗證目標物件與外部相依介面(在此為Url)的互動方式
我們預期
URL傳入近來只要是
http://e-traffic.taichung.gov.tw/DataAPI/api/TraStationAPI/7310
就要回傳此json內容
{'StationID':'7310','StationName':'雙溪','StationAddress':'22744新北市雙溪區新基里朝陽街 1 號','StationPhone':'02-24932980'}
TraStationWebHelper_Tests測試專案程式中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | using Microsoft.VisualStudio.TestTools.UnitTesting; using UnitTestApp1; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnitTestApp1_Tests1.Stub; using UnitTestApp1_Tests1.Mock; namespace UnitTestApp1.Tests { [TestClass()] public class TraStationWebHelper_Tests { [TestMethod()] public void GetSpecificTraStationInfo_Test() { //Arrange Real //var traStationWebHelper = new TraStationWebHelper(); //Arrange Fake(Stub) //var traStationWebHelperSub = new TraStationWebHelperStub(); //traStationWebHelperSub.FakeApiResult = new TraStationInfoDTO() //{ // StationID = "7310", // StationName = "雙溪", // StationAddress = "22744新北市雙溪區新基里朝陽街 1 號", // StationPhone = "02-24932980" //}; //Arrange Fake(Mock) var traStationWebHelperMock = new TraStationWebHelperMock(); var traStationWebHelper = new TraStationWebHelper(traStationWebHelperMock); //var StationID = "99"; var StationID = "7310"; var expected = "雙溪"; //Act //var actual = traStationWebHelper.GetSpecificTraStationInfo(StationID); //Act Fake(Stub) //var actual = traStationWebHelperSub.GetSpecificTraStationInfo(StationID); //Act Fake(Mock) var actual = traStationWebHelper.GetSpecificTraStationInfo(StationID); //Assert Assert.AreEqual(expected, actual.StationName); } } } |
此時執行單元測試可成功pass
通常腦筋想法會有點無法轉換過來
畢竟這根本就是自己在設定測試結果阿
當然會每測每過
根本100%會過
這樣子的測試有意義嗎
因為單元測試的重點一直以來
關注的就只有流程跟邏輯而不含外部依賴
若有需要測試到外部依賴就是要去做整合測試
就不算在單元測試範疇
Ref:
Mocks? Stubs? Or Shims?
Mock 入门,分析stub . mock区别
单元测试之Stub和Mock
使用 Microsoft Fakes 隔離測試中的程式碼
使用虛設常式隔離應用程式的各個組件,方便進行單元測試
使用填充碼隔離應用程式以進行單元測試
Microsoft Fakes 入門
Mocks vs Stubs vs Fakes In Unit Testing
Stub vs Mock – How to Make an Intelligent Choice in C#?
【單元測試的藝術】Chap 3: 透過虛設常式解決依賴問題
留言
張貼留言