Upgrade to Pro — share decks privately, control downloads, hide ads and more …

成功地測試失敗 Fail tests successfully - Talk Slides - PyCon Taiwan 2021

Keith Yang
December 28, 2021

成功地測試失敗 Fail tests successfully - Talk Slides - PyCon Taiwan 2021

摘要

這個講題會聚焦於從新創轉形為成長型企業時,在持續整合(Continuous Integration, CI)與交付流程中使用 Python 作為雲端主力開發時的「測試心得與踩雷案例」,也會展示不同的測試風格與手段以及它們的優缺點。來幫助產品交付的品質與 Python 2 升 3 時的過程。也是講者十年來從 nose test 寫到現在的工作需要將 pytest 與 Python unittest 混搭時的一些血淚心得。

說明

目標聽眾:正在考慮或已經在程式中加入各種測試的開發者。

分享完這議題後希望能帶來的效益:

- 為還在探索怎麼寫測試的人提供一些上手的點。
- **主要**為正在導入、整合測試流程的人提供:
- 一些可以注意的點。
- 一些不同風格測試的優缺點。
- 一些不同測試的方法與流程。
- 與已有 CI 測試流程的人分享怎麼做還是會錯。
- 為苦澀的青春帶來一些歡笑。

此講題對為何要寫測試的假設:
0. 生活值得更開心。
1. 加速開發。
2. 提升品質。
3. 節省維護與溝通成本。

影片
https://www.youtube.com/watch?v=lq4NPoouShY

會眾共筆
https://hackmd.io/@pycontw/2021/%2F%40pycontw%2FByIlgkYzt

Keith Yang

December 28, 2021
Tweet

More Decks by Keith Yang

Other Decks in Programming

Transcript

  1. # 專業期待 團隊常⾒的基本防線 - [x] 有 Code 的邏輯就有對應的測試。 - [x]

    有想到 DB Migration 有可能 downtime。 - [x] 確認了 Setting 的值是對的。
  2. Keith Yang • iCHEF Lead Backend Engineer • Taipei.py Co-organizer

    • 閱讀這個世界 • 打電動 • 滑板 • 登⼭ • 在家學吉它 最近只到得了雪⼭登⼭⼝
  3. assert 這個講題包含: • pytest, unittest and mock: True, their How-to:

    False • Coverage and parameterized: True, their How-to: False • Django and GraphQL: True, their How-to: surely False 
 • Micro-services testing: False • Some hands on integration tests: True • Data pipeline testing: False • Generating tests like Hypothesis: False Developer 導向: True 
 DevOps 導向: False (即使會帶到⼀些 CI/CD) 就不會提到微服務架構測試、資料處理的測試與⾃動產⽣測試
  4. assert | əˈsərt | ⼤綱如下: verb [reporting verb] 1 斷⾔,聲稱

    2 維護,堅持;主張擁有 3 顯⽰;確⽴ 1.簡介測試帶來的好處。 2.簡介⼀些⼯具。 3.深⼊沒有測試的壞處, 
 以及如何測好、測到。
  5. assert | əˈsərt | ⼤綱如下: verb [reporting verb] 1 斷⾔,聲稱

    2 維護,堅持;主張擁有 3 顯⽰;確⽴ 1.簡介測試帶來的好處。 2.簡介⼀些⼯具。 3.深⼊沒有測試的壞處, 
 以及如何測好、測到。
  6. Hypothesis 這個講題的假設 n. (plural hypotheses) 假說;前提 • 測試對⾝⼼健康好、對⼤家都好 • 組織與團隊、職涯,以及產品、專案、程式

    • 測試在組織裡常⾒的場景: 
 CI, QA, QE (Quality Engineering) 
 與 production testing • 簡單分享過去在為組織與團隊引⼊、實踐測試的困難 註:Production testing: 在正式釋出的環境進⾏測試。 
 例如:邊開⾶機邊測試⾶機會不會⾶。開⼀個真實店家測試剛上線的功能。
  7. 什麼是「品質」以及如何衡量? 可以⽤好幾本書來回答,這裡舉些與開發相關的情境 • 程式碼的測試覆蓋率:Code coverage • Pull-request 的 review ⼈數

    • 開發時 • 網⾴前端或⾏動裝置時的整合問題 • 上線前 QE 驗出來各種優先等級的問題數 • Hotfix 上線後發⽣的重⼤問題數量 • 測試的數量與⾓度 • ……族繁不勝枚舉。 覺得 Coverage 無法代表軟體開發的品質? 
 完全沒問題,再找其它更可靠的超前指標 
 若只能提供落後指標,說服⼒會有些不⾜
  8. 測試在組織裡常⾒的場景 CI, QA, QE, 與 production testing • CI 提報掉

    coverage 掉太多 • QA 問:「這個有 Unit test 嗎?」 
 -> 是不是在問:你不確定它會動,然後請我測看看嗎? • 跟第三⽅整合 API:這個只能在 production 上測喔
  9. 簡單分享過去在為組織與團隊引⼊、實踐測試的困難 • 到了⼀間信⽤卡公司做紅利點數,在 Windows 上跑起 Jenkins - 沒⼈⽤,就⾃⼰⽤:⼀個⼈的 Daily Build

    • 到了新創與 Nasdaq 軟體公司,每個新專案都建 CI 幫跑測試 • 不是要追求 Coverage 100%,只是想要⼜快⼜好,幫助增加開 發時的信⼼,⽼闆也不希望程式寫好了,上去不動了吧? ⼀些經歷分享
  10. assert | əˈsərt | ⼤綱如下: verb [reporting verb] 1 斷⾔,聲稱

    2 維護,堅持;主張擁有 3 顯⽰;確⽴ 1.簡介測試帶來的好處。 2.簡介⼀些⼯具。 3.深⼊沒有測試的壞處, 
 以及如何測好、測到。
  11. unittest.mock Django 內建也是繼承這個⾵格 • Python 內建函式庫 unittest 裡的 mock •

    mock | mäk | 
 vt. 模仿,仿效 
 n. 仿製品;贗品 • 可以⽤來仿製程式裡的⾏為
  12. Mock 整個 class 下的 AWS S3 -> 加速、省頻寬與錢 class RecoverS3AccountTests(MockS3Mixin,

    TransactionTestCase): def patch_mongo_conn(self): self.mongo_conn_patcher = patch("RecoverS3Account.mongo_conn") self.mock_connection = self.mongo_conn_patcher.start()
  13. pytest & coverage & vcrpy 簡介 • pytest: 
 簡單地寫測試,並在出錯時得到更詳細的資訊

    • Coverage: 
 計算並輸出程式覆蓋率 • vcrpy 
 錄下打出去的 requests,下次就可以⽤錄好的 response
  14. Jenkins 2 Pipeline 持續演化中 • 同時跑多個測試來加速, 
 但不是⽤ pytest-xdist •

    Build Docker image 時也會 檢查有沒有少 DB migration • 正打算把第三⽅平台相依與跑 太久的測試再抽出來 Frontend Team: GitHub Action iOS Team: Jenkins on local macOS instances QE Team: all kinds of tools 在 AWS 上⾃動開關幾⼗台 EC2-Fleet 跑測試
  15. assert | əˈsərt | ⼤綱如下: verb [reporting verb] 1 斷⾔,聲稱

    2 維護,堅持;主張擁有 3 顯⽰;確⽴ 1.簡介測試帶來的好處。 2.簡介⼀些⼯具。 3.深⼊沒有測試的壞處, 
 以及如何測好、測到。。
  16. 優先等級的問題 iCHEF 裡的⼯程品質量測度 • P0 當下需緊急處理 (Critical RIGHT NOW 🔥)

    • P1 關鍵,阻擋者 (Critical / Blocker) • P2 主要 (Major) • P3 次要的 (Minor)
  17. 濃縮版的 pytest 測試例⼦ class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id = "abcde"

    super().setUp() @ieatsfood_vcr.use_cassette("test_create_order_successfully.yaml") def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 66 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.00
  18. unittest 的 setUp ⾵格 class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id =

    "abcde" super().setUp() @ieatsfood_vcr.use_cassette("test_create_order_successfully.yaml") def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 66 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.00 ⽤來準備 class 下都會⽤到的測試的環境與資料
  19. 命名、區分測試的 setup 範圍 class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id = "abcde"

    super().setUp() @ieatsfood_vcr.use_cassette("test_create_order_successfully.yaml") def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 66 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.00 只跟這個測試相關的 setup 
 避免什麼東西都混在⼀起 setup,對⼤家都好 測試名稱很難取,⽅向是取得愈合情境愈清楚愈好 
 可能不要取 def test_it() ⽐較好
  20. ⼀個濃縮版的 pytest 測試 class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id = "abcde"

    super().setUp() @ieatsfood_vcr.use_cassette("test_create_order_successfully.yaml") def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 66 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.00 ⽤ vcrpy 錄下這次打第三⽅的 reqeusts.Response 
  21. ⼀個濃縮版的 pytest 測試 class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id = "abcde"

    super().setUp() @pytest.mark.vcr() def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 66 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.00 懶⼈寫法
  22. 還好有同事 @pytest. fi xture(scope="module") def vcr_con fi g(self): return {"match_on":

    ("method", "scheme", "host", "port", "path", "query", "body")} 
 ...
 @pytest.mark.vcr() def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 66 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.00
  23. 直接 assert 預期的固定值 class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id = "abcde"

    super().setUp() @ieatsfood_vcr.use_cassette("test_create_order_successfully.yaml") def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 20 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.0 你可能會想幫未來的⾃⼰與同事註解⼀下
  24. 直接 assert 預期的固定值 class IeatsfoodOrderTestCase(IeatsfoodTestCaseBase): def setUp(self): self.store_id = "abcde"

    super().setUp() @ieatsfood_vcr.use_cassette("test_create_order_successfully.yaml") def test_order_with_sub_total_promo_should_be_used_for_tax(self): self.set_up_menu_and_mapping_and_snapshots() order_data = self.get_ieatsfood_order(order_id="abcde") order_data["payment"]["sub_total_promo"] -= 20 ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) assert ieatsfood_order.tax == 8.0 # 8.0 = (100 - 20)/10.0 = (total order price - promo)/tax base 你可能會想幫未來的⾃⼰與同事註解⼀下,那個值是什麼意思
  25. 另⼀種不⽤註解的寫法 ... ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) expected_value = (total_order_price - sub_total_promo)

    / tax_base assert ieatsfood_order.tax == expected_value 能寫出不⽤註解就能讓⼈看懂的程式也蠻好,寫正常的功能時很推薦
  26. 但應避免寫測試時⼜寫了⼀次原來的程式邏輯 ... ieatsfood_order = IeatsfoodOrderReceiver().process(order_data) expected_value = (
 round(total_order_price) -

    fl oat(sub_total_promo) ) / user.tax_base[region] + item["discount"] * store[rate] assert ieatsfood_order.tax == expected_value 所以前⾯固定值加註解可能⽐這個例⼦好 所以很多 TDD 都會看到描述式的測式⾵格
  27. 這個處理 P0 的過程 當年有個第三⽅平台不太會算台灣的稅 1. 再急也要先確認規格與時程:要在什麼時候怎麼修 • ⾃⼰決定規格與時程的同時也表⽰⾃⼰擔下修壞的責任;若是有討 論過,⾄少⼤家⼀起承擔。 2.

    修完也會 QE 團隊使⽤ Staging 環境再驗證⼀次。 • 真的有測到其它意外的部份要修。 • 因為 Hotfix 也是最容易急著修壞正式環境的時刻,所以才要 更加⼩⼼。
  28. 這個處理 P0 的過程:沒有測試的話 1. 假如有在 QE 驗證階段發現問題: 
 預期會多花更多時間在上線前的來回 


    -> 修復需要更久的時間 
 -> 更多⽤⼾遇到問題 
 -> 客服收到更多電話與訊息 
 -> 這些都是組織、團隊與個⼈的損失 主要想看的:速度與品質 -> 成果與後果
  29. 這個處理 P0 的過程:沒有測試的話 2. 假如沒在 QE 驗證階段發現問題(或沒有這個階段): 
 預期上線沒修好問題,還可能引發新問題 


    -> 更多 Hotfix 需要搶修 
 -> 更緊張更難好好看、修問題 
 -> 進⼊惡性循環 <- 同時進⾏ 1.「修復需要更久的時間」到 
 組織的損失階段。 主要想看的:速度與品質 -> 成果與後果 從前從前,也是有 hot 🔥 過很多版本的學習 
 實務上怎麼避免也可以看⼀下 SRE 的書
  30. 另⼀個處理 P0 的過程 當年有個電⼦發票於年中加碼 1. 電⼦發票的字軌前綴(prefix)不夠⽤了 
 例:[IC, JK, ...

    RK],需於年中加 [IL, PO] 2. 原本設計是寫 code 跑 DB migration 
 把明年會⽤的字軌先寫進資料庫。 3. 所以就有兩種修法: A. 再寫個 code 再寫 test 再佈版補 B. 請另⼀個⼈⼀起 pair operating, 
 ⼿連幾個 production 環境下 SQL 加資料
  31. 另⼀個處理 P0 的過程 經過⼀點團隊討論,很快搞定。 1. 電⼦發票的字軌前綴(prefix)不夠⽤了 
 例:[IC, JK, ...

    RK],需於年中加 [IL, PO] 2. 原本設計是寫 code 跑 DB migration 
 把明年會⽤的字軌先寫進資料庫。 3. 所以就有兩種修法: A. 再寫個 code 再寫 test 再佈版補 B. 請另⼀個⼈⼀起 pair operating, 
 ⼿連幾個 production 環境下 SQL 加資料。[Done]
  32. VCR 的概念:儘可能貼測真實測資 • 使⽤ vcrpy ⽽沒有⾃⼰臨模⼿刻 dict 等資訊結構: 
 ⽤錄的(或

    copy paste)的資料結構與⽐較不容易錯。 • 臨模 dict 時 typo 與少⼀層 dict 都曾出錯過。 後來寧可 copy pate json 也不想再錯這種地⽅ 
 
 with Path("json_fixtures" / "ieatsfood_order.json").open() as json_file: ieatsfood_order = json.load(json_ fi le)
  33. For loop? 這次 P0 的 spec 說要單修第⼀種折扣出現的狀態 • 但看 log

    裡第三⽅ payload 過來的資料,是⼀個 list 的結構, 
 那是不是可能同時有第⼀種折扣與第⼆種折扣出現呢? • 因為有這個疑問,所以在解的時候就寫了 for loop 乖乖⼀種折扣⼀種折扣處理, 
 即使第三⽅平台的測試環境測不出來。 • 上線後果然出現混合折扣 <- 因為好好地寫程式避開了從⾃⼰⼿上出現另⼀個 P0 
 (即便可說是⾮戰之罪)。
  34. ⼀個 1 秒的測試需要多久完成? (改程式 + 測試)x 5 次 • CI:

    git push 20~30 分鐘 * 5 
 最多約 (30 * 60 * 60 + 1) * 5 = 540005 秒 • PyCharm: 4~6秒 * 5 切到⽬標 test 檔點 Run Test 
 最多約 (6 + 1) * 5 = 35 秒 • Command line: 1 秒切到 terminal 與 1 秒按上與 Enter 
 最多約 (1 + 1 + 1) * 5 = 15 秒 + focus 視窗切換 • Command line + watch: watcher 程式⾃動看改到⽬標檔案就跑測試 
 最多約 (1 + 1) * 5 = 10 秒
  35. ⼀個 1 秒的測試需要多久完成? (改程式 + 測試)x 5 次 • CI:

    git push 20~30 分鐘 * 5 
 最多約 (30 * 60 * 60 + 1) * 5 = 540005 秒 • PyCharm: 4~6秒 * 5 切到⽬標 test 檔點 Run Test 
 最多約 (6 + 1) * 5 = 35 秒 • Command line: 1 秒切到 terminal 與 1 秒按上與 Enter 
 最多約 (1 + 1 + 1) * 5 = 15 秒 • Command line + watch: watcher 程式⾃動看改到⽬標檔案就跑測試 
 最多約 (1 + 1) * 5 = 10 秒 https://xkcd.com/303/ 去吃飯或出⾨前跑個 CI 下去其實還不錯⽤
  36. ⼀個 1 秒的測試需要多久完成? (改程式 + 測試)x 5 次 • CI:

    git push 20~30 分鐘 * 5 
 最多約 (30 * 60 * 60 + 1) * 5 = 540005 秒 • PyCharm: 4~6秒 * 5 切到⽬標 test 檔點 Run Test 
 最多約 (6 + 1) * 5 = 35 秒 • Command line: 1 秒切到 terminal 與 1 秒按上與 Enter 
 最多約 (1 + 1 + 1) * 5 = 15 秒 • Command line + watch: watcher 程式⾃動看改到⽬標檔案就跑測試 
 最多約 (1 + 1) * 5 = 10 秒 (減少微 context switch) p2 'pytest -k order --lf -x cloud/ieatsfood' cloud/ieatsfood (By using simple PyPI package: p2) Hotfix 時每快⼀分鐘都是滿滿的感激
  37. @pytest.mark.django_db(transaction=True) @override_settings(LANGUAGE_CODE="zh_TW") @freeze_time(datetime(2021, 9, 2, tzinfo=timezone.utc)) # freezegun or time-machine

    @pytest.mark.parametrize("region", ["TW", "SG", "MY", "HK"]) def test_activation_ fl ow_should_trigger_noti fi cation(region): test_rows_ fi xture = [ [ # abc store, record created at 2pm. '2021/8/5', 'abc', ] ] with mock.patch('support.get_sheet_data', return_value=sheet_ fi xture(test_rows_ fi xture)): ApplyingFlow.update_from_sheet(region) abc_store = Applying.objects.get(store_id='abc', region=region) assert abc_store.sheet_row_created_at == '2021/8/5' with mock.patch( 'support.send_mail' ) as mock_send_mail: EmailService(applying).send_email() assert mock_send_mail.call_args[0][3] == set(["[email protected]"]) 對!我們⼀個測試有時會不客氣地有好幾個 assertion。 運算與時間有限之下,在便利、速度以及實⽤之間的取捨。 ⼀個貪⼼但⽕⼒全開的測試範例: - ⽤ mock 測 spreadsheet 以及寄 Email - 測國家區域 - 測⽇期時間
  38. 重複的測試會重複多少⾏? parameterized (on PyPI) @parameterized.expand( [ param(ApplyingStatus.FORM_NOT_FILLED, None), param(ApplyingStatus.FORM_FILLED, "2021-10-01"),

    param(ApplyingStatus.DONE, "2021-10-02"), ] ) def test_ieatsfood_query_applying_status(
 self, expected_status, planned_go_live_at
 ): self.integration.planned_go_live_at = planned_go_live_at self.integration.save(update_ fi elds=["planned_go_live_at"]) response = self.schema_execute(self.query_string) assert response.data["applyingStatus"] == expected_status 特別⽤這個套件, 
 主因是 @pytest.mark.parametrize 
 跟 Django 的 Unittest 蠻不合的,也確實好⽤。
  39. def test_ieatsfood_query_applying_status_form_not_ fi lled(self): self.integration.planned_go_live_at = None self.integration.save(update_ fi elds=["planned_go_live_at"])

    response = self.schema_execute(self.query_string) assert response.data["applyingStatus"] == ApplyingStatus.FORM_NOT_FILLED def test_ieatsfood_query_applying_status_form_ fi lled(self): self.integration.planned_go_live_at = "2021-10-01" self.integration.save(update_ fi elds=["planned_go_live_at"]) response = self.schema_execute(self.query_string) assert response.data["applyingStatus"] == ApplyingStatus.FORM_FILLED def test_ieatsfood_query_applying_status_done(self): self.integration.planned_go_live_at = "2021-10-02" self.integration.save(update_ fi elds=["planned_go_live_at"]) response = self.schema_execute(self.query_string) assert response.data["applyingStatus"] == ApplyingStatus.DONE not parameterized 重複的測試會重複多少⾏? 對眼睛與⼤腦都不好: 
 1. 愈多程式愈容易出錯 
 2. 需⽤⾁眼 diff (⽐較細微的不同)容易出錯
  40. Unit test 時也想要有整合測試法的效果: 測到第三⽅或其它 service 壞掉時標⽰ CI 失敗 但同時也會讓所有進⾏中的專案 CI

    過不了了,後來這樣解: @pytest.mark.integration_test class UnusedInvoiceIntegrationTestCase(InvoiceTestCaseBase): # pragma: no cover as integration tests. # Below is a pure copy of UnusedInvoiceTestCase content. def test_records_should_follow_time_ fi lter(self): self._test_records_should_follow_time_ fi lter() ... @pytest.mark.vcr class UnusedInvoiceTestCase(InvoiceTestCaseBase): def test_records_should_follow_time_ fi lter(self): self._test_records_should_follow_time_ fi lter() ... 讓只有不影響開發的 branch 不時跑⼀下 integration_test ⼀般的 CI 跑就⽤了 VCR 錄好的 response。 pytest -m "integration_test" pytest -m "not integration_test"
  41. 有時測試看起來只是把原來的設定值再測⼀次 • 表⾯看來只是測設定值好像沒有意義 • 防 typo! <- 真的有防守過 
 例如

    Vim 按 [ctrl-a] 
 就會貼⼼地幫你加⼀喔 <- 只是舉例勿戰 Emacs, PyCharm • 沒⼈想在上了 production 時才發現 
 P0 是因為 typo <- 當然發⽣過 user.py MAX = 10 ... test_user.py assert user.MAX == 10 有意義嗎?是關鍵值就很有。
  42. 忽然有⼀種 P0 也是很嚇⼈ 只測⼀間店,但撈資料時忘了加 filter by store_id @pytest. fi xture

    def two_stores(): return [ StoreFactory.create(customer_no='001'), StoreFactory.create(customer_no='002'), ] @pytest.mark.django_db def test_cloud_api_agent_sync_update_from_sheet(two_customers): with mock.patch('support.cloud_api_agent.logger') as mock_logger: CloudAPIAgent.sync_update_from_sheet() assert mock_logger.error.call_args[0][0] == [ ('002', 'invalid_store_id'), ] assert Store.objects.count_valid_stores() == 1 下次的預防⽅式: 只測⼀間店測不出來的問題,就測兩間。 測試很簡單。 難的都是早知道 -- 資深⼯程師的期待。 對了,這個測試也測到 logger.error 
 有沒有被呼到的細度。有需要嗎?
  43. Coverage 說有測到的 if ⼩⼼原來以為測到的部分,在同⼀⾏塞⼊太多邏輯 # Coverage: this line is tested.

    Oh Really? store = StoreFactory.create(customer_no=value) if value else None if value: store = StoreFactory.create(customer_no=value) else: store = None # Coverage: this line is not tested 最後回到「沒測到會怎樣」的討論
  44. 這個處理 P0 的過程:沒有測試的話 1. 假如有在 QE 驗證階段發現問題: 
 預期會多花更多時間在上線前的來回 


    -> 修復需要更久的時間 
 -> 更多⽤⼾遇到問題 
 -> 客服收到更多電話與訊息 
 -> 這些都是組織、團隊與個⼈的損失 主要想看的:速度與品質 -> 成果與後果
  45. 這個處理 P0 的過程:沒有測試的話 1. 假如有在 QE 驗證階段發現問題: 
 預期會多花更多時間在上線前的來回 


    -> 修復需要更久的時間 
 -> 更多⽤⼾遇到問題 
 -> 客服收到更多電話與訊息 
 -> 這些都是組織、團隊與個⼈的損失 主要想看的:速度與品質 -> 成果與後果
  46. 這個處理 P0 的過程:沒有測試的話 2. 假如沒在 QE 驗證階段發現問題(或沒有這個階段): 
 預期上線沒修好問題,還可能引發新問題 


    -> 更多 Hotfix 需要搶修 
 -> 更緊張更難好好看、修問題 
 -> 惡性循環 <- 同時進⾏ 1.「修復需要更久的時間」 
 到組織的損失階段。 主要想看的:速度與品質 -> 成果與後果
  47. 這個處理 P0 的過程:沒有測試的話 2. 假如沒在 QE 驗證階段發現問題(或沒有這個階段): 
 預期上線沒修好問題,還可能引發新問題 


    -> 更多 Hotfix 需要搶修 
 -> 更緊張更難好好看、修問題 
 -> 惡性循環 <- 同時進⾏ 1.「修復需要更久的時間」 主要想看的:速度與品質 -> 成果與後果
  48. 不好的「快」可能不是真的快 慢起來就知道的幻想數學⿁故事 • ⼀週上線,四週除錯重新上了六版,廣告⼀開始就撒滿, 新⽤⼾四週後從⼀開始 1000 剩不到 20 持續使⽤, 


    還有 30 個技術問題要處理。 • 兩週上線,兩週除錯重新上了三版,廣告邊修邊撒, 
 新⽤⼾四週後從⼀開始 600 剩不到 100 持續使⽤, 
 還有 10 個技術問題要處理。 單位視喜好可以乘上百上千,感覺與想像都會有些不同。