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. Keith Yang
    成功地測試失敗

    Fail Tests Successfully
    PyCon Taiwan 2021

    View Slide

  2. 先來逛逛

    最近⾝邊跟測試有關的場景

    View Slide

  3. 最近團隊 Coverage 的不正常發揮:90.84% -> 93.01%

    View Slide

  4. 升級超⽼舊套件時,CI 裡 326 個測試表⽰抗議:
    🆇
    還好有 Passed: 5504 tests

    View Slide

  5. Jenkins 有沒有好好⼯作:最⾼同時執⾏近 100 個 job,喔!
    Build

    Test

    Deploy

    View Slide

  6. 發 pull-request 時的內容樣版

    .github/pull_request_template.md

    View Slide

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


    - [x] 有想到 DB Migration 有可能 downtime。


    - [x] 確認了 Setting 的值是對的。

    View Slide

  8. Keith Yang
    • iCHEF Lead Backend Engineer


    • Taipei.py Co-organizer • 閱讀這個世界


    • 打電動


    • 滑板


    • 登⼭


    • 在家學吉它
    最近只到得了雪⼭登⼭⼝

    View Slide

  9. iCHEF

    View Slide

  10. Taipei.py
    (疫情休息中)

    看年底能不能再
    辦⼀場

    View Slide

  11. 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)
    就不會提到微服務架構測試、資料處理的測試與⾃動產⽣測試

    View Slide

  12. View Slide

  13. assert | əˈsərt | ⼤綱如下:
    verb [reporting verb]


    1 斷⾔,聲稱 2 維護,堅持;主張擁有 3 顯⽰;確⽴
    1.簡介測試帶來的好處。


    2.簡介⼀些⼯具。


    3.深⼊沒有測試的壞處,

    以及如何測好、測到。

    View Slide

  14. assert | əˈsərt | ⼤綱如下:
    verb [reporting verb]


    1 斷⾔,聲稱 2 維護,堅持;主張擁有 3 顯⽰;確⽴
    1.簡介測試帶來的好處。


    2.簡介⼀些⼯具。


    3.深⼊沒有測試的壞處,

    以及如何測好、測到。

    View Slide

  15. Hypothesis 這個講題的假設
    n. (plural hypotheses)


    假說;前提
    • 測試對⾝⼼健康好、對⼤家都好


    • 組織與團隊、職涯,以及產品、專案、程式


    • 測試在組織裡常⾒的場景:

    CI, QA, QE (Quality Engineering)

    與 production testing


    • 簡單分享過去在為組織與團隊引⼊、實踐測試的困難
    註:Production testing: 在正式釋出的環境進⾏測試。

    例如:邊開⾶機邊測試⾶機會不會⾶。開⼀個真實店家測試剛上線的功能。

    View Slide

  16. • 組織對於產品的觀點:交付品質,以及速度


    • 職涯對於價值的觀點:程式與專案(    )的掌握度

    (  )、產出量(  )


    • 團隊看著的也是產品、專案、程式的速度與品質
    組織與團隊、職涯,以及產品、專案、程式

    View Slide

  17. • 組織對於產品的觀點:交付品質,以及速度


    • 職涯對於價值的觀點:程式與專案(關鍵價值)的掌握度

    (品質)、產出量(速度)


    • 團隊看著的也是產品、專案、程式的速度與品質
    組織與團隊、職涯,以及產品、專案、程式
    主要想看的:關鍵價值的速度與品質

    View Slide

  18. 什麼是「品質」以及如何衡量?
    可以⽤好幾本書來回答,這裡舉些與開發相關的情境
    • 程式碼的測試覆蓋率:Code coverage


    • Pull-request 的 review ⼈數


    • 開發時


    • 網⾴前端或⾏動裝置時的整合問題


    • 上線前 QE 驗出來各種優先等級的問題數


    • Hotfix 上線後發⽣的重⼤問題數量


    • 測試的數量與⾓度


    • ……族繁不勝枚舉。
    覺得 Coverage 無法代表軟體開發的品質?

    完全沒問題,再找其它更可靠的超前指標

    若只能提供落後指標,說服⼒會有些不⾜

    View Slide

  19. 測試在組織裡常⾒的場景
    CI, QA, QE, 與 production testing
    • CI 提報掉 coverage 掉太多


    • QA 問:「這個有 Unit test 嗎?」

    ->
    是不是在問:你不確定它會動,然後請我測看看嗎?


    • 跟第三⽅整合 API:這個只能在 production 上測喔

    View Slide

  20. 簡單分享過去在為組織與團隊引⼊、實踐測試的困難
    • 到了⼀間信⽤卡公司做紅利點數,在 Windows 上跑起 Jenkins
    - 沒⼈⽤,就⾃⼰⽤:⼀個⼈的 Daily Build


    • 到了新創與 Nasdaq 軟體公司,每個新專案都建 CI 幫跑測試


    • 不是要追求 Coverage 100%,只是想要⼜快⼜好,幫助增加開
    發時的信⼼,⽼闆也不希望程式寫好了,上去不動了吧?
    ⼀些經歷分享

    View Slide

  21. 簡單分享過去在為組織與團隊引⼊、實踐測試的困難
    可能也沒那個時間去說服。
    你是老闆的話可能也不希望被說服。
    • 團隊或主管需要說服的話:他們能接受的點是什麼?


    • 也許先⽤ Jenkins 很快設定出⼀個很基本的 CI


    • 效率與品質是實作的⼀部份,不是伴⼿禮可有可無


    • 真的很急的專案,絕對需要取捨的部份,通常不⽤說服

    View Slide

  22. 簡單分享過去在為組織與團隊引⼊、實踐測試的困難
    • 團隊或主管需要說服的話:他們能接受的點是什麼?


    • 也許先⽤ Jenkins 很快設定出⼀個很基本的 CI


    • 效率與品質是實作的⼀部份,不是伴⼿禮可有可無


    • 真的很急的專案,絕對需要取捨的部份,通常不⽤說服
    ⽼⽣⻑談:找對組織、團隊與⼈(上司、同事)就不難;也最難

    View Slide

  23. assert | əˈsərt | ⼤綱如下:
    verb [reporting verb]


    1 斷⾔,聲稱 2 維護,堅持;主張擁有 3 顯⽰;確⽴
    1.簡介測試帶來的好處。


    2.簡介⼀些⼯具。


    3.深⼊沒有測試的壞處,

    以及如何測好、測到。

    View Slide

  24. unittest.mock
    Django 內建也是繼承這個⾵格
    • Python 內建函式庫 unittest 裡的 mock


    • mock | mäk |

    vt. 模仿,仿效

    n. 仿製品;贗品


    • 可以⽤來仿製程式裡的⾏為

    View Slide

  25. 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()

    View Slide

  26. pytest & coverage & vcrpy
    簡介
    • pytest:

    簡單地寫測試,並在出錯時得到更詳細的資訊


    • Coverage:

    計算並輸出程式覆蓋率


    • vcrpy

    錄下打出去的 requests,下次就可以⽤錄好的 response

    View Slide

  27. 我們還裝了什麼
    • pytest-cov


    • pytest-vcr


    • pytest-django


    pytest & coverage & vcrpy

    View Slide

  28. 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 跑測試

    View Slide

  29. assert | əˈsərt | ⼤綱如下:
    verb [reporting verb]


    1 斷⾔,聲稱 2 維護,堅持;主張擁有 3 顯⽰;確⽴
    1.簡介測試帶來的好處。


    2.簡介⼀些⼯具。


    3.深⼊沒有測試的壞處,

    以及如何測好、測到。。

    View Slide

  30. 優先等級的問題
    iCHEF 裡的⼯程品質量測度
    • P0 當下需緊急處理 (Critical RIGHT NOW 🔥)


    • P1 關鍵,阻擋者 (Critical / Blocker)


    • P2 主要 (Major)


    • P3 次要的 (Minor)

    View Slide

  31. ⼀個處理 P0 的測試

    View Slide

  32. 濃縮版的 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

    View Slide

  33. 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 下都會⽤到的測試的環境與資料

    View Slide

  34. 命名、區分測試的 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() ⽐較好

    View Slide

  35. ⼀個濃縮版的 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 

    View Slide

  36. ⼀個濃縮版的 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
    懶⼈寫法

    View Slide

  37. ⼩⼼ vcrpy default 的 match ⾏為
    特別是搭配 GraphQL 時!

    View Slide

  38. 還好有同事
    @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

    View Slide

  39. 直接 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
    你可能會想幫未來的⾃⼰與同事註解⼀下

    View Slide

  40. 直接 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
    你可能會想幫未來的⾃⼰與同事註解⼀下,那個值是什麼意思

    View Slide

  41. 另⼀種不⽤註解的寫法
    ...
    ieatsfood_order = IeatsfoodOrderReceiver().process(order_data)
    expected_value = (total_order_price - sub_total_promo) / tax_base
    assert ieatsfood_order.tax == expected_value
    能寫出不⽤註解就能讓⼈看懂的程式也蠻好,寫正常的功能時很推薦

    View Slide

  42. 但應避免寫測試時⼜寫了⼀次原來的程式邏輯
    ...
    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 都會看到描述式的測式⾵格

    View Slide

  43. 這個處理 P0 的測試
    想表達的是

    其實跟平常寫測試⼀樣,

    情況允許的話,

    不要因為著急⽽跳過了測試。

    View Slide

  44. 這個處理 P0 的過程
    當年有個第三⽅平台不太會算台灣的稅
    1. 再急也要先確認規格與時程:要在什麼時候怎麼修


    • ⾃⼰決定規格與時程的同時也表⽰⾃⼰擔下修壞的責任;若是有討
    論過,⾄少⼤家⼀起承擔。


    2. 修完也會 QE 團隊使⽤ Staging 環境再驗證⼀次。


    • 真的有測到其它意外的部份要修。


    • 因為 Hotfix 也是最容易急著修壞正式環境的時刻,所以才要
    更加⼩⼼。

    View Slide

  45. 這個處理 P0 的過程:沒有測試的話
    1. 假如有在 QE 驗證階段發現問題:

    預期會多花更多時間在上線前的來回

    -> 修復需要更久的時間

    -> 更多⽤⼾遇到問題

    -> 客服收到更多電話與訊息

    -> 這些都是組織、團隊與個⼈的損失
    主要想看的:速度與品質 -> 成果與後果

    View Slide

  46. 這個處理 P0 的過程:沒有測試的話
    2. 假如沒在 QE 驗證階段發現問題(或沒有這個階段):

    預期上線沒修好問題,還可能引發新問題

    -> 更多 Hotfix 需要搶修

    -> 更緊張更難好好看、修問題

    -> 進⼊惡性循環 <- 同時進⾏ 1.「修復需要更久的時間」到

    組織的損失階段。
    主要想看的:速度與品質 -> 成果與後果
    從前從前,也是有 hot 🔥 過很多版本的學習

    實務上怎麼避免也可以看⼀下 SRE 的書

    View Slide

  47. 另⼀個處理 P0 的過程
    當年有個電⼦發票於年中加碼
    1. 電⼦發票的字軌前綴(prefix)不夠⽤了

    例:[IC, JK,
    ...
    RK],需於年中加 [IL, PO]


    2. 原本設計是寫 code 跑 DB migration

    把明年會⽤的字軌先寫進資料庫。


    3. 所以就有兩種修法:


    A. 再寫個 code 再寫 test 再佈版補


    B. 請另⼀個⼈⼀起 pair operating,

    ⼿連幾個 production 環境下 SQL 加資料

    View Slide

  48. 另⼀個處理 P0 的過程
    經過⼀點團隊討論,很快搞定。
    1. 電⼦發票的字軌前綴(prefix)不夠⽤了

    例:[IC, JK,
    ...
    RK],需於年中加 [IL, PO]


    2. 原本設計是寫 code 跑 DB migration

    把明年會⽤的字軌先寫進資料庫。


    3. 所以就有兩種修法:


    A. 再寫個 code 再寫 test 再佈版補


    B. 請另⼀個⼈⼀起 pair operating,

    ⼿連幾個 production 環境下 SQL 加資料。[Done]

    View Slide

  49. 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)

    View Slide

  50. For loop?
    這次 P0 的 spec 說要單修第⼀種折扣出現的狀態
    • 但看 log 裡第三⽅ payload 過來的資料,是⼀個 list 的結構,

    那是不是可能同時有第⼀種折扣與第⼆種折扣出現呢?


    • 因為有這個疑問,所以在解的時候就寫了 for loop 乖乖⼀種折扣⼀種折扣處理,

    即使第三⽅平台的測試環境測不出來。


    • 上線後果然出現混合折扣 <- 因為好好地寫程式避開了從⾃⼰⼿上出現另⼀個 P0

    (即便可說是⾮戰之罪)。

    View Slide

  51. ⼀個 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 秒


    View Slide

  52. ⼀個 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 下去其實還不錯⽤

    View Slide

  53. ⼀個 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 時每快⼀分鐘都是滿滿的感激

    View Slide

  54. ⼀些 P1~P3 的開發、補洞測試

    View Slide

  55. @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


    - 測國家區域


    - 測⽇期時間

    View Slide

  56. 重複的測試會重複多少⾏?
    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 蠻不合的,也確實好⽤。

    View Slide

  57. 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 (⽐較細微的不同)容易出錯

    View Slide

  58. 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"


    View Slide

  59. 有時測試看起來只是把原來的設定值再測⼀次
    • 表⾯看來只是測設定值好像沒有意義


    • 防 typo! <- 真的有防守過

    例如 Vim 按 [ctrl-a]

    就會貼⼼地幫你加⼀喔 <- 只是舉例勿戰 Emacs, PyCharm


    • 沒⼈想在上了 production 時才發現

    P0 是因為 typo
    <-
    當然發⽣過
    user.py


    MAX = 10


    ...


    test_user.py


    assert user.MAX == 10


    有意義嗎?是關鍵值就很有。

    View Slide

  60. 忽然有⼀種 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

    有沒有被呼到的細度。有需要嗎?

    View Slide

  61. 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
    最後回到「沒測到會怎樣」的討論

    View Slide

  62. 沒測到會怎樣?

    View Slide

  63. 這個處理 P0 的過程:沒有測試的話
    1. 假如有在 QE 驗證階段發現問題:

    預期會多花更多時間在上線前的來回

    -> 修復需要更久的時間

    -> 更多⽤⼾遇到問題

    -> 客服收到更多電話與訊息

    -> 這些都是組織、團隊與個⼈的損失
    主要想看的:速度與品質 -> 成果與後果

    View Slide

  64. 這個處理 P0 的過程:沒有測試的話
    1. 假如有在 QE 驗證階段發現問題:

    預期會多花更多時間在上線前的來回

    -> 修復需要更久的時間

    -> 更多⽤⼾遇到問題

    -> 客服收到更多電話與訊息

    -> 這些都是組織、團隊與個⼈的損失
    主要想看的:速度與品質 -> 成果與後果

    View Slide

  65. 這個處理 P0 的過程:沒有測試的話
    2. 假如沒在 QE 驗證階段發現問題(或沒有這個階段):

    預期上線沒修好問題,還可能引發新問題

    -> 更多 Hotfix 需要搶修

    -> 更緊張更難好好看、修問題

    -> 惡性循環 <- 同時進⾏ 1.「修復需要更久的時間」

    到組織的損失階段。
    主要想看的:速度與品質 -> 成果與後果

    View Slide

  66. 這個處理 P0 的過程:沒有測試的話
    2. 假如沒在 QE 驗證階段發現問題(或沒有這個階段):

    預期上線沒修好問題,還可能引發新問題

    -> 更多 Hotfix 需要搶修

    -> 更緊張更難好好看、修問題

    -> 惡性循環 <- 同時進⾏ 1.「修復需要更久的時間」
    主要想看的:速度與品質 -> 成果與後果

    View Slide

  67. 有測試就⼀定沒問題嗎?
    當然不是這樣,最後總是靠組織裡各種專業團隊⼀起努⼒。


    只是試著寫好測試應該可說是軟體⼯程職涯裡的基本功之
    ⼀,從愈寫愈好的程式到愈寫愈好的測試,試著把軟體最
    核⼼的關鍵之⼀做好。


    有測試時⽽出事時,⼀來有根本知道⾃⼰哪些是有測到哪些
    是沒測到的,⼆來假如連基本的功能都沒測,QE 團隊⼜
    要問了:「這個有 Unit test 嗎?」

    View Slide

  68. 什麼都要測試嗎?
    當然無法這樣,最後總是靠組織裡各種專業團隊⼀起努⼒。


    對團隊⽽⾔,判斷哪些是關鍵的測試並有測到更重要。


    關鍵的⾏為並非只有程式碼的長相,例如以為只是沒測
    log,其實隱含著那⼀段邏輯都沒測到。

    View Slide

  69. 不好的「快」可能不是真的快
    慢起來就知道的幻想數學⿁故事
    • ⼀週上線,四週除錯重新上了六版,廣告⼀開始就撒滿,
    新⽤⼾四週後從⼀開始 1000 剩不到 20 持續使⽤,

    還有 30 個技術問題要處理。


    • 兩週上線,兩週除錯重新上了三版,廣告邊修邊撒,

    新⽤⼾四週後從⼀開始 600 剩不到 100 持續使⽤,

    還有 10 個技術問題要處理。
    單位視喜好可以乘上百上千,感覺與想像都會有些不同。

    View Slide

  70. 總結
    預防勝於治療
    • 測試是為了速度與品質,不好的快不是真的快

    因為急所以才要寫好基本該測到的程式


    • 簡明的程式與測試很棒,

    持續追求「有測到關鍵邏輯」

    View Slide

  71. View Slide

  72. Thanks

    View Slide