作者: paul

.net core 應用程式 在AppService進行dotnet dump

.net core 應用程式 在AppService進行dotnet dump

當應用程式有High Memory 或是High Cpu的現象時,若希望對應用程式進行Runtime的分析,以前在IIS上常常進行的是在工作管理員對w3wp進行傾印,移到雲端又是Container形式後,跟以往 在windows的經驗不大一樣,不得其門而入,以下記錄一下作法

在Container For AppService的環境下要能進行Dump,會有以下 步驟

Container安裝dotnet-dump 的tool

# 前略...
# Open port 2222 for SSH access
EXPOSE 80 2222

# dotnet
RUN apk add icu-libs tzdata
RUN apk --update add libgdiplus ttf-dejavu
RUN apk add --no-cache bash curl icu-libs libc6-compat \
    && curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh \
    && bash /tmp/dotnet-install.sh --channel 8.0 --install-dir /usr/share/dotnet 

# 安裝 dotnet-dump
RUN /usr/share/dotnet/dotnet tool install -g dotnet-dump \
    && ln -s /root/.dotnet/tools/dotnet-dump /usr/bin/dotnet-dump \
    && dotnet --info

# 後略...

確認Docker Build成功

通常你的webapp有多個 instance,請先設定環境變數增加以下 設定為true

WEBSITES_ENABLE_APP_SERVICE_STORAGE

這個設定是讓 多個instance的/home目錄共用,後續可以透過kudu的newui進行FileManager下載Dump檔案

透過進階工具連到開發工具(不是上面的SSH,這個外層的SSH應該主要是連到Container Host的主機,要連到指定的Instance的話,可以進到Kudu去指定Instance)

接著ssh連進去 確認Container內dotnet-dump版本

/home/LogFiles# dotnet-dump --version
9.0.553101+5b61d34de04d6100e6003415f7d7e9c4b971afd4

確認指定ok後,查詢process id

ps aux | grep dotnet
16 root 5h23 dotnet /app/example.dll
10891 root 0:00 grep dotnet

進行dump collect,注意,此步驟在linux container進行時,並不會中斷process或重啟(這跟我們之前w3wp.exe的經驗不太一樣,不過或許還是會間接造成伺服器比較忙錄,所以可以離峰進行)

dotnet-dump collect  -p [process-id]  --type Full  --diag

Writing full to /home/LogFiles/core_20250211_010126
Complete

成功後

透過kudu 的FileManager就可以找到檔案 進行下載分析~~

Reference :

https://techcommunity.microsoft.com/blog/appsonazureblog/how-to-collect-net-core-dump-on-linux-web-app/2260713

支援TestContainer資料庫整合自動化測試的Clean Architecture專案模版-2024年版總結

支援TestContainer資料庫整合自動化測試的Clean Architecture專案模版-2024年版總結

在前幾份工作透過課程學過單元測試與重構,工作領域就是較多的傳統三層式架構(ex: Presentation-Service-Repository),直到前一份接觸到了實務的Clean Architecture方案開始(約莫三年),發現了C#開發的新天地,終於有一種先前課程單元測試與重構、極速開發學到的技巧有機會應用到工作上的感覺。

因此最近在多個產品領域逐步試驗抽換,有些產品是從0-1,我覺得相對容易,只要開發Pattern形成,其實風格跟文化就會自動形成。

大概的要素如下:

  1. Clean Architecture的專案模版(EFCore .NET+ MediatR-CQRS)
  2. 自動化 IntegrationTest + UnitTest

Clean Architecture的專案模板(EFCore .NET+ MediatR-CQRS)

傳統 N 層架構與 Clean Architecture 的差異

最大的不同在於 依賴注入(DI) 的引入後,Infrastructure 層參考 Application 層,而 API 層 則參考 Application 層MediatR 物件。MediatR 提供了一種 標準化的行為,讓使用層(Client 或 API)可以透過抽象方法進行呼叫(類似 Invoke Action)。透過 IRequest + IRequestHandler,請求會標準化地傳遞至 Application 層 執行。這樣的設計提高了 可測試性,因為大部分的 業務邏輯內聚在 Application 層,不像傳統架構可能將部分邏輯散落在 Web API 等層級中。當然,開發者仍然可以 不遵循原則,把邏輯寫得分散,這就需要有意識地去避免這種情況。在這樣的架構下,Web API 的 Controller 只需專注於與 IHttpContext 交互,或處理 Request-Level 事務,例如: Mapping Command / Query 物件

到了 Application 層,應該只剩下 業務邏輯,不應與 HttpContext 這類原生或底層物件耦合。這樣能大幅提高 Application 層的可測試性。反之Infrastructure 層 應該完全封裝對外部系統(如 Redis、資料庫(DB)、其他第三方套件)的實作,外部專案只能透過 Interface 操作,確保解耦與可維護性。

Clean Architecture 中,DbContext 屬於 Infrastructure 層,而 Entities 則歸屬 Domain 層。實際上,Entity 本質上就是資料庫表的物件對應。如果 Schema 設計符合 Use Case,資料操作起來就會 更直覺,就像在處理業務邏輯,而不只是冷冰冰的 SQL 操作(如 Insert、Update、Delete)。

使用了EntityFramework後,雖然我們可以透過 DI 綁定 IDBContext,讓任意使用端操作資料庫。這樣的做法 雖然方便,但不建議長期使用。或許在 開發初期,這種方式有助於 自動化測試,因為 TDD 產生的測試可以直接驗證資料庫行為。然而,當系統進入 重構階段,應將 IDBContext 介面Application 層 提取到 Service 層,這樣的調整 將會非常輕鬆,同時也能讓架構更符合 Clean Architecture 原則

自動化 IntegrationTest + UnitTest

單元測試的 3A 原則(Arrange, Act, Assert),以及 Mock、Fake、Stub 等技巧固然重要,但這裡想強調的是一個 觀念上的轉變。我認為 單元測試不僅僅是函式層級的測試,而是 只要測試對象可以完全控制,就能算是單元測試。如果一個函式綁定了資料庫操作,是否還能進行單元測試?我認為可以。只要能在測試前準備好資料庫,並在測試結束時清除資料,那麼就能夠 更真實地模擬函式的行為

這樣的做法避免了繁瑣的 Mock 設定,因此 TestContainer 應運而生。過去,準備一個測試用資料庫相對麻煩,不僅需要在本機安裝,還要在 CI/CD 環境中對應不同的資料庫實例。但隨著 Docker 的出現,以及 微軟支援 Docker 版 SQL Server,這個問題得到了有效解決。因此,因為「資料庫」難以”測試”已不應再成為藉口

曾有人問我,許多業務邏輯因為過度依賴資料庫,而導致難以進行良好的單元測試。常見的解法是 將資料庫操作封裝進 IRepository,但接下來的問題是:我們是否應該對 IRepository 進行 Mock?我的結論是:看需求與團隊接受程度。如果我們可以 自動建置資料庫,且 IRepository 主要對接資料庫,那麼就應該讓測試驗證實際的資料庫操作。如果不測試,我認為測試是不完整的,這樣的測試結果也不太令人安心。至於 IProxy 介接外部 API 的部分,我認為這屬於 異質系統,通常交由外部團隊維護,因此建議 Mock 掉

TestContainer 與 Integration Test

在了解 TestContainer 的概念後,我們的目標是讓 每次測試(包含 CI Pipeline)都能進行 Integration Test。因此,我建議採取以下做法:

1. 透過 Docker 自動建立資料庫

2. 自動建置 Schema(可參考 Repository 中 IntegrationTest 的 BeforeRun)

3. 自動升級資料庫版本,確保 Application 依賴的資料庫狀態正確

如此一來,撰寫每個 Test Case 時,數據層都能確保已被測試。

最後,我建議搭配 Database.Reset 套件(目前支援 SQL ServerPostgreSQL),以便快速重置資料庫,確保每次測試環境的一致性。這樣一來,我們就能實踐 「整合單元測試」(Integration Unit Test)。

維護成本與 Schema 同步

維護成本主要來自於 Schema 的同步,這部分可由 DBA 維護,或 團隊自行定義同步方式。這裡沒有標準做法,重點是讓流程 舒適、易於維護,才能長期執行下去。

這些做法在我們團隊已經試驗超過半年,整體來說 可行,團隊成員也已經習慣撰寫測試,並且包含 數據庫驗證測試。目前,我正協助 其他產品團隊 導入 自動化數據庫測試流,調整適合他們的方案。

當然,這個過程難以避免 一定程度的重構,而且不同方案各有風險。由於大多數系統缺乏良好的測試保護,導致改造充滿挑戰。然而,透過 測試技巧,可以逐步 抽離 Interface(這是我認為最重要的一步)。

整體來看,主要的挑戰在於 改造策略與推進進度,但這 並非技術或測試不可行的問題。這部分有機會的話,我會在後續進一步分享。

Clean Architecture 的一般化方案

為了讓 微軟 C# Application 能夠 快速理解並導入 Clean Architecture,我對這套架構進行了一定程度的 一般化整理,並已經在 GitHub 上建立範本。這不僅是個紀錄,也能作為 範本,方便有需要的團隊 直接導入或參考

連結如下: https://github.com/pin0513/ECAppForCleanArchitecture

todo: 會持續將 MediatR的案例整合進去哦

Nodejs並支持 React的CMS解決方案(todo study)

Nodejs並支持 React的CMS解決方案(todo study)

在您提到的选项中,以下的 CMS 可以通过 Docker Compose 构建,并使用 MariaDB 进行数据库连接,类似于 WordPress 的部署方式:

1. Strapi

Docker 支持:Strapi 提供了官方的 Docker 镜像,并有示例供参考。

MariaDB 支持:Strapi 默认支持 MariaDB,您可以在配置中指定。

部署示例

以下是一个使用 Docker Compose 部署 Strapi 和 MariaDB 的示例:

version: '3'

services:

  strapi:

    image: strapi/strapi

    ports:

      - '1337:1337'

    environment:

      DATABASE_CLIENT: mysql

      DATABASE_HOST: db

      DATABASE_PORT: 3306

      DATABASE_NAME: strapi

      DATABASE_USERNAME: root

      DATABASE_PASSWORD: example

    depends_on:

      - db

  db:

    image: mariadb:latest

    environment:

      MYSQL_ROOT_PASSWORD: example

      MYSQL_DATABASE: strapi

    volumes:

      - db_data:/var/lib/mysql

volumes:

  db_data:

使用步骤

1. 将上述内容保存为 docker-compose.yml 文件。

2. 在终端中运行 docker-compose up -d 启动服务。

3. 访问 http://localhost:1337,按照提示完成 Strapi 的初始设置。

2. KeystoneJS

Docker 支持:虽然 KeystoneJS 没有官方的 Docker 镜像,但您可以自己编写 Dockerfile,或者使用社区提供的镜像。

MariaDB 支持:通过 Prisma,KeystoneJS 可以连接到 MariaDB。

部署示例

由于需要自定义配置,建议参考以下步骤:

1. 编写 Dockerfile:创建一个包含您的 KeystoneJS 应用的 Dockerfile。

2. 编写 docker-compose.yml:配置 KeystoneJS 服务和 MariaDB 服务。

3. 运行:使用 docker-compose up -d 启动。

3. Payload CMS

Docker 支持:Payload CMS 提供了 Docker 部署指南。

MariaDB 支持:支持 MySQL 兼容的数据库,包括 MariaDB。

部署示例

version: '3'

services:

  payload:

    image: payloadcms/payload

    ports:

      - '3000:3000'

    environment:

      PAYLOAD_CONFIG_PATH: ./payload.config.js

      DATABASE_URL: mysql://root:example@db:3306/payload

    volumes:

      - ./:/usr/src/app

    depends_on:

      - db

  db:

    image: mariadb:latest

    environment:

      MYSQL_ROOT_PASSWORD: example

      MYSQL_DATABASE: payload

    volumes:

      - db_data:/var/lib/mysql

volumes:

  db_data:

4. Webiny

Docker 支持:Webiny 可以使用 Docker 部署,但主要设计为无服务器架构。

MariaDB 支持:需要进行额外的配置,可能不如其他选项直接。

推荐方案

鉴于您的需求,Strapi 是最直接和方便的选择。它提供了官方的 Docker 镜像,默认支持 MariaDB,而且社区资源丰富。

参考资料

Strapi 官方文档Strapi – Getting Started with Docker

Payload CMS Docker 指南Payload CMS – Docker Deployment

注意事项

环境变量:确保在 docker-compose.yml 中正确配置环境变量,如数据库用户名、密码和数据库名称。

数据持久化:使用 Docker 卷(volumes)来持久化数据库数据,防止数据在容器重启时丢失。

端口映射:确认服务的端口映射,避免与本地其他服务冲突。

我覺得wordpress的優勢在於 plugin很多,很多金流都有套件可以用…看來要找時間來試一下這些架站是否可以用來取代wordpress了

2024 WordPress自動架站心得-DockerCompose with AI

2024 WordPress自動架站心得-DockerCompose with AI

最近一個朋友想架官網,我第一個想到的還是wordpress

雖然以技術而言,php感覺已經不是主流(主流是GO, Python….etc),但以能做到CMS網站的解決方案而言,wordpress看起來還是滿強勢,重點是前後台就一站solution包好了,不用去想前後端分離什麼的 ,所以還是試著先搭配AI去安裝看看吧~

第一步,申請Gmail,為你雲端服務商申請有一個信箱 可以註冊

Azure跟 AWS都有免費 12個月,一定大小的size的vm規格可以申請,其實大同小異,這邊我選了熟悉的GCP

第二步 登入gmail後,直接申請 GCP,這邊其實 因為 都是google的服務,所以很順的申請完成!

他一開始會有一個MyFirstProject,一時間無法改名這個Project,所以我就立即創建一個新的Project,免費額度內好像可以建立共12個Project。然後申請服務的時候,會需要信用卡,但他說不會刷額度,在300美元額度內,你不啟用,都可以試用他們的服務,就算額度用完,他也不會跟你直接扣款。

第三步,開啟Compute-Engine,建立一個VM,這時我只有選一個core2, 8g ram的機器,其他都幾乎沒改,連地域都放在us(其實可以挑一下),一直下一步,等待一會兒就可以看到他背景建置好了!

第四步

透過 webssh連進去(這個比起當年還要建立ssh金鑰,上傳 Server才能用 iterm連線 的時代已經方便超級多了!

第五步

問Chatgpt要怎麼wordpress架站,首先先安裝Docker

第一個prompt如下

"Linux instance-20241202-162827 6.1.0-27-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.115-1 (2024-11-01) x86_64

我要怎麼安裝docker "

他回應我如下,我就不全列了,反正AI都會教好教滿,若你貼錯了 把錯誤訊息再丟回去問他,他也會試圖幫你解決

第六步: 安裝Docker-Compose

prompt
"docker-compose  怎麼安裝"

盡量要描述具體,所以我連ssh登入hint給的os資訊都一併給 ,這樣好讓他判斷要給我什麼安裝指令

其實先 前我因為相關經驗,有透過個別指令 安裝過Docker,DB,wordpress各自的container,而這次我希望一鍵安裝,所以使用Docker-Componse,同時我希望還是會有一個實體路徑,以確保備份還有未來轉移遷站的議題。

Prompt
"我要建立一個docker file 然後
docker file 希望可以建立一個wordpress跟mariadb,然後他們的實體路徑希望綁到/mnt/db跟/mnt/wordpress"
version: '3.9'
services:
  wordpress:
    build: .
    container_name: wordpress
    ports:
      - "8080:80"  # 將主機的 8080 端口映射到容器的 80 端口
    volumes:
      - /mnt/wordpress:/var/www/html  # 綁定實體目錄
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress_password #記得改一下
      WORDPRESS_DB_NAME: wordpress 
    depends_on:
      - db

  db:
    image: mariadb:latest
    container_name: mariadb-container
    volumes:
      - /mnt/db:/var/lib/mysql  # 綁定實體目錄
    environment:
      MYSQL_ROOT_PASSWORD: root_password #記得改一下
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress  
      MYSQL_PASSWORD: wordpress_password #記得改一下

volumes:
  wordpress_data:
    driver: local
  db_data:
    driver: local

其他

這時若你linux的OS時區跟語系有問題的話(optional),你也可以請他給你指令

最後,其實執行 “sudo docker-Compose up -d”,就可以一次把這些服務弄起來了

再用 “sudo docker ps -a”檢查一下是否有跑不起來的服務

註: 若你想在vm內驗證服務是否有up起來,你也可以再多chatgpt指令,他會教你

第7步: 設定GCP防火牆 (理論上不用管vm的firewall,不然很可能會不小心讓你連ssh都連不進去)

預設GCP是給 https-server所以只有開放443 port,只要點擊 vm清單上的外部ip旁的網路介面,就可以去設定他的防火牆,這時只要加入tcp: port 8080 允許的規則

小插曲: 原面安裝完跑docker-componse up以後,我看沒有錯誤訊息,但我看連外部ip 80 port都連不進去後,一直繞在怎麼允許80 port連線,還想不透去猜 是不是mariadb還是wordpress上配置不對,後來才發現是docker上port mapping是8080: 80,代表是vm的8080 port要connect到container的80 port,所以才發現gcp我怎麼開80port都還是連不到的原因XD,果然開了8080 port就通了!

你的網站理論上就可以運作了,這時只要選擇語系按安裝,就可以一鍵安裝好了(歡呼 ~)

安裝完如下:

後續其實要做到一個比較完整的官網,還有包含domain的註冊,SSL的配置就不細說了

以上架站其實搭配Chatgpt已經可以一站式的完成所有的工程(若不是firewall,弄到好可能不用10分鐘),而不用像當年我必須對每一個安裝的主題(例如mariadb、docker)去做查詢別人的文章或相關的指令 ,真的超級smooth。

有的時候都覺得AI革的命,絕對不只是軟體設計師 ,未來會不會SRE等指令結構化工程相關的人員可能都漸漸被AI取代掉了?或許分工上,不再那麼的壁壘分明了吧,因為這些工具能透過AI自動化就能被介面整合,人力需求肯定會比以前少的

至於wordpress的關鍵字SEO設定,這些屬於工程外的細節就另開一篇章吧~

跑Scrum 或敏捷Agile於產品開發專案管理之心路歷程 初期到穩定期

跑Scrum 或敏捷Agile於產品開發專案管理之心路歷程 初期到穩定期

在產品開發的初期階段,Scrum 和敏捷的方法真的有必要嗎?這是個值得探討的問題。敏捷和 Scrum 在軟體業界很普遍,很多團隊透過看板、每日站會、短衝(Sprint)和回顧會議(Retro)來落實敏捷的理念。然而,這些形式化的流程能不能真的解決實際問題?特別是在突發需求頻繁出現的情況下,可能需要更靈活的處理方式。

初期階段的挑戰:探索與定義基礎

在從 0 到 1 的產品開發過程中,關鍵在於快速探索和實驗,收斂出「理想中的產品」——一個真正解決用戶痛點並具有長期價值的產品。這個階段的主要工作是設計規劃和開發執行,但產品還處於不穩定階段時,是否需要導入 Scrum 的周期性制度,其實需要根據實際情況來決定。

我認為,這個階段更需要有經驗的團隊成員來主導方向。他們要能掌握專案的核心,建立必要的開發規範,比如命名規則、測試流程、自動化測試和部署等等。這些規範是為未來的開發文化打基礎的,比形式化的敏捷流程更為重要。

核心目標:快速驗證與凝聚共識

在產品初期,快速驗證概念和想法是最重要的。我們需要團隊內部的高度共識,而不應該盲目執行敏捷框架裡的所有流程。

例如,每日站會是有價值的,因為它能幫助我們快速對齊進度和方向。但像定期的計劃會議或回顧會議,未必是必需的。開會的重點應該放在解決問題和確認目標,而不是為了執行某種固定的流程。

在實際操作中,我們可能花一整週時間設計和驗證基礎架構,而這些會議更像是靈活的討論,目的是確保團隊方向一致。無論是用什麼形式,只要能達到溝通和協作的效果就可以了。

成員特質與團隊目標

初期階段的成功很大程度上取決於團隊的特質和目標是否一致。這個時期的團隊需要具備創新和探索精神,所有人要能夠靈活應變並投入實驗。如果有成員無法融入這樣的節奏,可能會拖慢整個團隊。

產品穩定後:調整工作方式

當產品進入穩定期,比如 1.0 版本上線,開始有用戶使用,並且 CICD 流程已經成熟時,我們需要根據新的需求來調整工作模式。

穩定期的開發節奏會更加規律,我們可能引入更多的敏捷實踐,比如 Sprint 計劃、回顧會議和里程碑規劃等。這個時候,Scrum 的實踐方式會更有價值,因為穩定的節奏能幫助團隊同步開發進度和目標。

以下是我們實踐的一些要素:

1. 看板(Kanban):確保工作可視化,通常會追蹤兩個 Sprint 內的所有任務。我們使用 Excel 或簡單的工具來輔助管理,確保每個人都能鳥瞰全局。

2. 每日站會(Daily Standup):關注當前的障礙和即將進入看板的任務,解決問題更高效。

3. 計劃會議(Planning):對齊目標和分工,並確保每個成員理解需求。

4. 細化會議(Refinement):拆解功能並進行估算,確保大型需求能夠分階段完成。

5. 展示與回顧(Demo and Retro):每個 Sprint 結束時進行成果展示,前端、後端和文件更新都需要同步完成,確保所有人對進展有清楚認識。

6. 知識管理與文件同步:使用 Excel、Figma 或 Markdown 結合 Wiki 平台,記錄關鍵資訊,讓團隊成員能快速上手並保持靈活。

結論:形式為目標服務

敏捷和 Scrum 的核心是幫助團隊靈活應對變化並提升效率。在產品開發初期,重要的是設計適合當下情境的工作方式,而不是執著於敏捷的形式化流程。專注於快速驗證、團隊共識和基礎規範的建立,能讓團隊在不確定性中保持效率。

當產品進入穩定期後,可以逐步引入更多敏捷的元素,但前提是每項實踐都能真正為團隊和產品帶來價值。

c# 冗餘的 await 調用

c# 冗餘的 await 調用

最近在CodeReview一些程式碼的時候,看到這行字

冗餘的 await 調用

• 如果結果已經是 Task,不需要重複 await,這樣可以簡化代碼。

想起來有時候我們確實在一些方法並沒有回傳值的問題而定義了回傳的Task

但這時候使用端仍往往習慣去await這個Task

這時建議的移除await,”似乎”是反習慣性的建議

因此我想說,用一些具體的例子,看一下在async 方法下 不去await一個 Task方法時,會不會有什麼影響

結論: 不會有影響~

簡易的程式碼如下:

Program.cs

Console.WriteLine("1");

var test = new Test();

test.Method1();

Console.WriteLine("2");

Console.ReadLine();


public class Test
{
    async Task Method2()
    {
        Console.WriteLine("3");
    }
    
    async Task<int> Method3()
    {
        Console.WriteLine("6");
        return 7;
    }
    
    public void Method1()
    {
        Method2();
        Console.WriteLine("4");
        var value = Method3();
        Console.WriteLine("5");
        Console.WriteLine(value.Result);
    }
}


Console的執行順序結果會打印 出什麼結果?

[ChatGPT閒聊]自動化測試要關注什麼?

[ChatGPT閒聊]自動化測試要關注什麼?

我問 ChatGPT,它回答我:

在自動化測試中需要關注以下幾個重點:

1. 測試範圍選擇(Test Scope Selection)

選擇適合自動化的測試用例,通常會選擇那些重複性高、穩定、且在多個版本中都會執行的用例,如回歸測試、API測試等。對於容易變動且難以自動化的部分,則可能更適合手動測試

2. 測試腳本的可維護性(Maintainability of Test Scripts)

要確保自動化測試腳本易於維護,編寫腳本時應保持簡潔、模組化並遵循乾淨的代碼原則。當應用程式更新時,測試腳本需要能夠方便地做出調整。

3. 測試用例的穩定性(Stability of Test Cases)

自動化測試應該要具備穩定性,避免 flaky tests(不穩定測試),即某些測試結果偶爾會通過,偶爾會失敗的情況。可以通過確保測試環境穩定、避免依賴外部因素等方式來提高測試的穩定性。

4. 測試數據管理(Test Data Management)

自動化測試需要有效管理測試數據,確保測試數據是獨立且可控的。這意味著測試腳本不應該依賴於生產環境數據,並且應該能夠生成或重置測試數據。

5. 報告和記錄(Reporting and Logging)

測試報告應該能夠詳細展示測試結果,包括測試通過與失敗的用例、失敗的原因、錯誤日誌等,以方便快速定位問題並作出相應調整。

6. 異常處理和重試機制(Exception Handling and Retry Mechanism)

自動化測試腳本應該具有良好的異常處理機制,能夠在遇到錯誤時記錄詳細信息,並且在合適的情況下進行重試。這可以避免因網絡不穩定或暫時性問題導致的測試失敗。

7. 持續集成(Continuous Integration, CI)

自動化測試應與持續集成工具(如 Jenkins、GitLab CI 等)相結合,確保測試能夠在代碼變更時及時執行,並且能自動生成測試報告,以方便追蹤代碼品質。

8. 測試環境的隔離和一致性(Environment Isolation and Consistency)

測試環境應該與開發和生產環境相隔離,並且要保持一致性,以避免測試結果受到環境差異的影響。可以使用 Docker 或虛擬機等工具來確保環境的一致性。

9. 測試效率和執行速度(Test Efficiency and Speed)

自動化測試需要在合理的時間內完成,過長的執行時間可能會影響開發和發布節奏。因此需要平衡測試覆蓋率和測試效率,並嘗試使用並行執行或分佈式測試來加速測試流程。

10. 選擇合適的測試工具和框架(Choosing the Right Tools and Frameworks)

根據項目的需求、技術棧和團隊熟悉程度來選擇合適的自動化測試工具和框架,如 Selenium、Appium、Cypress、Robot Framework 等。不同工具有其優勢和適用場景,選擇適合的工具能提高開發效率。

關注這些重點,可以有效地構建和維護自動化測試流程,提高測試效率和產品質量。

以上是ChatGPT整理的摘要,若能在產品開發階段就關注可測試性,其實 對於後續導入測試框架會有事半功倍的效果

以下是我的觀點,幾個應該前期產品設計與開發就該涉獵的重點:

首先,最基本的是測試獨立性。雖然環境設定或第三方服務可能有些麻煩,但這些通常可以透過 Mocking 或在指定環境下關閉來解決,視測試重點而定。然而,更麻煩的是如何確保測試數據的獨立性,這直接關係到測試案例的穩定性。如果每次生成測試數據時,數據數量不一致(例如,原本只有 1 筆,重跑後變成多筆,甚至超過 10 次導致新分頁出現),那麼原本會通過的案例可能在第 10 次執行時突然失敗。

設計及維護測試數據本身是一項高成本的活動,因此在開發初期理清數據流相當重要。數據往往存在相依性,例如,有人員資訊但沒有權限資訊,某些功能就無法運作。因此,確定最小可動範圍是功能性測試的第一步,通常專案中有整合可重覆與重置的資料庫整合測試框架,我認為極有助於這一塊議題的發展(以我們是使用了.Net Core + EntityFramework + Respawn套件 (https://www.nuget.org/packages/respawn)達成數據可測試性框架,有興趣可以自行再深入了解。接著,測試的獨立性要求在同一版本或不同版本間能重複執行持續執行。例如,若隨版本更新而需要同步更新測試數據,否則新功能可能會因不相容的數據而壞掉。因此,功能測試時也要考慮數據的相容性。雖然設計上沒有絕對的好壞,但一致性與簡單易懂是設計的原則。

接著,關於程式數據生成,在日常功能測試時,RD 可以設計資料生成機制,將測試任務和迴歸測試任務拓展至 RD 層級,要求他們在各環境中確保重複驗證功能性。這樣 RD 就會設計出透過程式生成數據的機制(大家都怕麻煩),對測試資料的建立與生成很有幫助。例如,我們近期產品開發時,我會規劃一個「示範客戶/公司」,由示範客戶下 來建立獨立的使用者,自有的權限,自有的相關數據,而不會影響其他客戶,並透過程式生成示範產品的基本可動數據,確保每次上版時能自動更新,這樣就能銜接更進階的測試,特別是涉及外部同步的資料。最好隨時備妥可重複的模擬資料,確保測試流程順暢。

註:以上示範了在 Multi-Tenant 架構下,如何在前期設計中獨立控制資料欄位。如果不是採用這種架構,也可以從資料庫層面切割客戶,這樣做其實沒有問題,主要還是跟架構成本有關。但一旦確立了標準的 schema,接下來就是如何確保系統在最小可行範圍內運作。

總結來說,一切還是圍繞在成本效益比(C/P 值)上。多數專案或產品因為看不到短期效益而猶豫不前,這種情況很常見。就像決定一家餐廳的存亡時,還在考慮菜單字體好不好看,這樣很難說服自己行動。因此,效益和成本必須一起考慮。建立自動化測試也是如此,如果產品在撰寫和執行測試案例時不夠靈活,造成困難,這就會成為一大門檻。大多數阻礙你前進的問題,不是工具怎麼用,而是當你開始動手時,不知道能不能處理後續的麻煩。

Playwright在Azure Pipeline配置

Playwright在Azure Pipeline配置

2024年10月2日,颱風天雖然無風無雨,但還是放了颱風假,只好待在家裡研究最近上保哥的 Playwright 課程後的一些心得和小成果。

從在本地環境使用 Node.js 和 TypeScript 建立測試案例到執行,已經有很多文件和參考資料,所以這部分就先略過。

不過對於自動化框架已經有這麼多套了後,為什麼目前選擇playwright呢?上完課初步瞭解後,我自己整理有以下幾點

  1. playwright簡化了selector,重新設計了定位器,符合無障礙網頁設計規範,與autowait,並可以透過 playwright生態與Api支援下 ,測試可讀性也大幅增加,配合強大工具進行錄製,大幅降低了e2e測試案例寫code的困難度,也降低了維護上的成本。
  2. 強大的Mocking的機制,還有諸多黑科技機制 ,不旦可以mocking api , browser 的storage, 有機會達成更多深入的驗證情境(例如pass 人類、簡訊等雙因子驗證)
  3. 支持所有顯示主流語言,包含java , c# , nodejsd的type script,且可以透過 框架一鍵切換,文件也很完整。
  4. 微軟爸爸金主萬歲

其實,E2E的測試,還有一個就是文化面的觀點,就是所有的測試都必須跟RD雙向奔赴,一面測試能夠做到反饋,產品主動優化,這樣正向循環才有助於產品品質的發展。

作為一個自動化測試框架,通常後續整合到專案或產品上有兩個實作方向:

1. 作為前端專案的介面整合測試

在這種情況下,測試案例的版控權限需要開放到前端專案中。因此,如果開發人員以外的人(如 QA、PM)要協助編修測試案例,可能會頻繁變動專案。而且可能還需要在建置過程中處理 localhost 的啟動及套件測試驗證。這樣的測試其實有點像界面的單元測試,只是更依賴於前端專案,並且還要考慮相關的服務或其他環境的設定。

當然這種方式的好處在於,測試流程可以隨著介面的變更一起調整,與前端工程師的工作息息相關。因此,測試案例的防守範圍包含了 RD,可以隨著功能特性(feature)的調整,達到與版本部署的同步。

但維護成本也相對高(跟專案綁在一起的關係)

2. 作為產品介面的回歸測試

這種方式著重於將較為穩定且不常變動的介面優先建置測試案例。目前,我們專案正朝著這樣的 E2E 測試流程發展,希望能針對不同環境(如 tst、uat、preprod、prod)進行測試。藉由Azure Pileine配置獨立的 E2E 測試專案及執行指令,可以靈活地測試不同環境的標的。

獨立了 test 專案後,測試team(當然也可以RD、PM都一起來寫XD)可以依據不同環境規劃測試案例的目錄。透過 npx playwright test 指令,可以方便地測試指定的案例目錄及不同瀏覽器參數。這種方式相當便捷(先前也玩過 Cypress),並且獨立版控的測試專案也帶來了一些好處,例如可以在 CI 流程中靈活分配,無論是每次上版到不同環境時進行或是定期測試、在指定條件下執行測試,都能自由安排。

這次我希望達到的是上述第二種切入點的做法,因為我們的 E2E 測試希望從使用者操作的角度進行,以 “指定環境” 的 Web 站台為測試對象。

因此我們將BASE_URL(測試站台的進入點)作為 參數由外部傳入

另外,因為考慮了playwright的覆用性,因此也把playwright的大部分Task包成另一個 Azure Template

這樣一來,不同環境就可以建置不同的Pipeline去定義 參數後再傳入PipelineTemplate執行標準的測試 Task

Main Pipeline

(以下範例移除了公司的標記,可以依有抽出參數的部分自由參考運用)


pool:
  vmImage: ubuntu-latest

trigger:
  branches:
    include:
      - 'main'

resources:
  repositories:
  - repository: PipelineTemplates
    type: git
    name: {版控}/Pipeline-Templates
  - repository: E2ETesting
    type: git
    name: {專案}-End2EndTesting

name: $(Build.BuildId)

stages:

  - stage: qa
    displayName: 'Run Automation Test - QA'
    dependsOn: []
    jobs:
      - template: template/playwright-template.yml@PipelineTemplates
        parameters:
          BASE_URL: 'https://tst-{站台}/zh-TW/home' #測試站點
          End2EndProjectKey: E2ETesting  #測試專案版控Key
          End2EndProjectName: {專案}-End2EndTesting #測試專案目錄名稱(用於組合路徑用)

PipelineTemplates

(作為Function可以被不同的pipeline參考執行,注意,這個是 playwright-template.yml的內容,另外一個檔案)


parameters:
  - name: BASE_URL
    type: string
  - name: End2EndProjectKey
    type: string
  - name: End2EndProjectName
    type: string

jobs:

  - job: test
    displayName: Run Playwright Tests
    steps:
      - download: none
      - checkout: self
        persistCredentials: true    
      - checkout: ${{ parameters.End2EndProjectKey }}
        persistCredentials: true

      - script: |
          ls $(System.DefaultWorkingDirectory)
        displayName: "Print Working Dir"

      - task: NodeTool@0
        displayName: 'Use Node version 18'
        inputs:
          versionSpec: 18.x

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          ls .
        displayName: "Cd E2E Folder and ls folder "

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          ls .
          npm ci
        displayName: "NPM Install"

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          ls .
          pwd
          echo $(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/browsers
          cd /home/vsts/
          PLAYWRIGHT_BROWSERS_PATH=/home/vsts/
          npx playwright install --with-deps
           #note 不要用sudo安裝,會裝到/root去專案跑的時候會找不到browser driver
        displayName: "Playwright Install"

      - script: |
          echo "BASE_URL= ${{ parameters.BASE_URL }}"
        displayName: "Print Variables"

      - script: |
          cd ${{ parameters.End2EndProjectName }}
          BASE_URL=${{ parameters.BASE_URL }} CI=true PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-results/results.xml npx playwright test
        displayName: "Run Playwright Tests"
        continueOnError: true

      - task: ArchiveFiles@2
        displayName: 'Add playwright-report to Archive'
        inputs:
          rootFolderOrFile: '$(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/playwright-report/'
          archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip'

      - task: ArchiveFiles@2
        displayName: 'Add test-results to Archive'
        inputs:
          rootFolderOrFile: '$(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/test-results/'
          archiveFile: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip'
          replaceExistingArchive: false

      - task: PublishPipelineArtifact@1
        displayName: 'Publish Pipeline Artifacts'
        inputs:
          targetPath: '$(Agent.TempDirectory)/$(Build.BuildId)_$(System.JobAttempt)$(System.StageAttempt).zip'
          artifact: pipeline-artifacts

      - task: PublishTestResults@2
        inputs:
          testResultsFormat: 'JUnit'
          testResultsFiles: '*.xml'
          searchFolder: '$(Pipeline.Workspace)/s/${{ parameters.End2EndProjectName }}/test-results'
          testRunTitle: 'Playwright ADO Demo - $(System.StageName)'
        displayName: 'Publish Test Results'

成果

壓力測試到底在關注什麼?

壓力測試到底在關注什麼?

最近請同事開始使用工具做壓力測試,一開始講目的是為了”知道”我們的API能否承受一定的使用者使用我們的系統,至於多少使用者呢?假定是1000個線上用戶為目標。但過了一陣子,發現大家似乎 對於要測什麼,怎麼測,測完後的行動,壓力測試跟效能測試差異,都還有一些疑慮。導致無法有效的進行。

先講”相對”最簡單的議題,怎麼測,首先應該是依賴工具,有各種技術方案,有的用JMeter,有的用Script的Tool例如K6。端看想要如何測試往下延續或保持這種壓測的節奏。

JMeter的考量可能是有GUI的壓測劇情設定編排的畫面,對於比較不寫程式的,也有機會去達成壓測的工作。一樣可以做到算是API間的相依,像是從Login拿Token,再拿Token去打 其他API做驗證後的邏輯。這些都做的到,報表也有整合好可以看到一些圖表數據。這些數據我就先不討論了,都是有定義的。

其他工具例如K6,就可以讓你用JavaScript的方式寫腳本,方便用程式的方式 自動化 的,重覆使用化的去設計你的壓測流程,這是好處,不過就會有一點點門檻。(這對程式有點基礎的就學習上相對簡易很多)

再來一層就是我們要討論 為什麼要壓力測試,壓力測試的對象從不同的面象又可以做一個粗淺的分類,例如:

  1. front-end 或是 back-end
  2. 元件或是 end 2 end 測試
  3. 單一協定架構或混合架構。

上述分類不代表什麼,只是說明你可能可以整體來看你測試對象的分類為何 ,因為能錨定你的對象跟你選擇的工具,腳本的寫法都有些關係。不過我認為,這些還是可以概念上分成由上而下拆解,還是由下而上堆疊來驗證。

由上而下,不外乎還是把你的測試對象視為一個黑盒子。可能你知道中間的構成是什麼,也可能不知道或不確定,因此透過 測試,可以幫助你推論 瓶頸在何處,但都要數據來證明。愈複雜的組成有可能有愈多的依賴。因此你一開始愈來,拆的成本就愈高。

由下而上,我認為是你可以透過很多碎片/單元化的可驗證組件逐一測試。這些分開的成本也不小。疊到符合”使用場景”要花的時間,是你需要考慮的。例如假如知道一隻極度單純的API查詢 資料庫1次的數據(先知道環境依賴的跟與業務邏輯無關的部分),其實你可以往下驗證到,例如你的登入,背後可能是1隻API結合查詢 3次 ,寫入1次 ,這樣的子的邏輯複雜度。在你設定的壓力狀況下的成績如何。再來逐步的疊加可能登入Api+查詢Api複合性的商業邏輯,不過注意必須合理,若網站所有Api不考慮相關性,相依性 平均的去壓測他們,這樣的測試也是既耗時也浪費的。

因此從上述的方向,並不是說你只能走一個方向,而是你要怎麼透過”不同的角度”去找答案!

以API的部分,舉個例子:

一開始 可能設想由上而下,我們現在電商網站最常被使用是登入嗎?可能不是,大家要嘛就已經登入了,user資訊存在cookie中 ,要嘛大家就在逛網站,而不用急著登入。那麼登入真的是最需要被驗證的接口嗎?其實可能不是。這些都端看你網站內的流程如何設計。那往對應到流程後,背後有哪些”最相關的Api分析出來後,就可能是你首要的驗證對象。

因此 使用者場景=分析=>關鍵路徑(常用不一定關鍵)=分解=>背後關鍵的API順序。這個過程去識別出測試對象,就是很重要的。

大則流程,細則程式,你也可以想象,這是一個識別風險的過程,並透過科學的方式證明他們是或不是風險,每次開發好一個API功能後,可能先單獨 驗證這隻API的效能,再接著從這次 新增的Api去往外推論影響的範圍。

最後,聊一下由下而上的概念,這邊舉個例子,就是測試參數到底怎麼決定比較好,看了k6的vus跟duration還有iteration後,我發現測試上會跟現實狀況會有脫勾的情況,例如你要測 2000 concurrent user,你的想像是一秒內同時打 2000個請求,都能被消耗,並能被快速的執行完成,是這樣嗎?往往 使用者情境不見得是這樣,就算是搶票,我們還是可以區分出讀寫比重不一定一樣的行為模式,因此結合使用者情境或是能透過你的情境去解釋,就會各自有差別,不過我想說的以本機測試為例,我們其實是希望知道受測單元的極限。例如1個直接回傳 200的api接口,在一台PC等級的筆電上,能驗證的vus同時併發量就是440vus(因為超過可能就會”環境”failed,是server被reset connection的那種),然後平均每秒 處理請求數是多少,這些都可以先針對你的技術框架有一個單元極限值,然後再往 疊加業務邏輯的方式,去收斂那個參數邊界,最後找到一個合理 測試參數,接著就是固定這個變數,往再去尋找其他可控 變數,例如組合不同邏輯下,對效能、可用性的影響為何 。

知道作法跟想做的方向後,再來看我們要怎麼解讀壓力測試API的結果來回答我們的疑問,現學現賣一下,不外乎想知道兩個面向

  1. Latency(延遲)
  2. Availability(可用性)

從Latency 延遲角度,可以結合Performance的角度就是為了知道有壓力源下的回應時間的分佈狀況。例如你可以想像在早上8:00的時候,使用系統的人最多,可能有1000人,平均等待時間會從100人的平均3秒成長到平均10秒 ,而這樣的延遲是否可以”忍受”。若不能忍受就要提高服務器的性能或是做其他的分流機制。

從Availability可用性的角度,主要是想知道API是否會中止服務,而導致關鍵流程無法完成。因此我認為就是在看 Total跟Failed的比例。假如Fail是3次 ,但是是100萬次的3次,跟10次 裡的3次 ,狀況可能完全不一樣。(但這不代表 100萬的3次 不重要)

可以想像一個情境,假如你有一個搶票的API,公告12:00開始可以進來搶100張票,結果假如你只有一台主機服務,結果因為開搶那一瞬間,湧進來10000個人來搶。瞬間把API打到當機,背後的資料庫也阻塞中止新連線,結果整個網站不要說進來了,連前面有成功進來的人也無法往下走到”金流”的流程,那麼就是典型的可用性出了很大的問題。

而這些都是可以”提前”大膽的假設,透過壓測工具來求證。那麼如何使用工具來驗證場景呢?K6官網有整理了幾種壓測的情境,來因應上述的場景
1.Smoke Test
2.Average Test
3.Stress Test
4.Spike Test
5.Soak Test

其中或許我們只要分辨出,SmokeTest目的是用小單位去驗收你的測試腳本是否正確,先行排除在重點之外。

而Average, Stress, Spike都是在給定一個壓力源(這個也是要設計的)下 ,壓力可能是量,可能是時間長(Soak),到底回應時間以及可用性如何。

上面先啦賽了這麼多,就是希望有助於結構性的先理解我們面對的東西是什麼,概念跟術語是什麼,先做好定義同步共識後,往下做的Action才有理有據,而每一次做的壓測都有如一場實驗,留下的數據,就算是沒有瓶頸的發現,那保留下來的可能那組”安全”參數、數據上的經驗都應該要能夠被延續,而不是把每個壓測都視為那麼單一的事件去操作。每個產品,每個階段,每個功能都不盡相同,但分析與解決問題的脈落其實都有一套科學思維的pattern,數據也有解讀的方式。解讀出來後,如何往下分析瓶頸(依賴)跟重構,那屬於壓測後的獨立工作,就先不在這邊一併討論了。

Server Set Cookie Cross Domain的雷

Server Set Cookie Cross Domain的雷

繼API的CORS的問題後

現在又遇到了一個set cookie的問題,下面的測試順序是反過來的,最早是遇到無法設定到Browser/Application的Cookie,後來發現可以設定的方式後,改實驗多種Domain的組合…

首先本機測試Host先改成模擬前後端分離站台的部署模式

      response.cookies.set('refreshToken', refreshToken, {
        path: '/',
        maxAge: 60 * 60 * 24 * 7,
      });

      response.cookies.set('domainTokenByMiddleWare', refreshToken, {
        path: '/',
        domain: '.azure-paul.com',
        maxAge: 60 * 60 * 24 * 7,
        secure: true,
      });

set-cookie成功!

接著改換 azure-paul.net

      response.cookies.set('refreshToken', refreshToken, {
        path: '/',
        domain: '.azure-paul.net',   
        maxAge: 60 * 60 * 24 * 7,
      });

      response.cookies.set('domainTokenByMiddleWare', refreshToken, {
        path: '/',
        domain: '.azure-paul.net',
        maxAge: 60 * 60 * 24 * 7,
        secure: true,
      });

一樣可以

透過 client端直接呼叫api server response的set-cookie也work

最後我改成這個domain

     response.cookies.set('refreshToken', refreshToken, {
        path: '/',
        domain: '.azurewebsites.net',   
        maxAge: 60 * 60 * 24 * 7,
      });

      response.cookies.set('domainTokenByMiddleWare', refreshToken, {
        path: '/',
        domain: '.azurewebsites.net',
        maxAge: 60 * 60 * 24 * 7,
        secure: true,
      });

結果無情的被拒絕set-cookie

真的沒有寫進去…驚嘆號上的hint是寫著..”current host url invalid”

這樣看來跟.net or .com沒有關係 ,倒是跟azurewebsites.net的domain被列入了卡控 名單有關係

似乎..這跟我”一開始”在/etc/hosts改成dev.azure.com 一樣,會直接被chrome卡掉

繞了一大圈後…

小結論如下
CORS跟Cookie ,若以不同domain要做到CrossDomain的存取,必須繞路避開大網站的domain
(如上azure-paul.com, azure-paul.net passed)

或是直接以NGINX Proxy建成xxxx.yyyy.com + xxxx.yyyy.com/backend,直接就可以不用遇到這種雷…

若有其他發現或知道真相的人歡迎一起分享指教~~

自行備註:

針對azurewebsites.net的set sub-domain的問題其實有人分享過 ,只是一開始哪會找這個方向XD

https://learn.microsoft.com/en-us/answers/questions/360645/cannot-set-a-subdomain-friendly-cookie

竟然是真的跟這邊的名單有關?(半信半疑)

https://publicsuffix.org/list/public_suffix_list.dat

此文的解法 真的也是綁到名單之外的Domain