HTTP 請求測試:開發者的生存工具

已發表: 2022-03-11

當測試套件不可行時該怎麼辦

有時我們——程序員和/或我們的客戶——只有有限的資源來編寫預期的可交付成果和該可交付成果的自動化測試。 當應用程序足夠小時,您可以偷工減料並跳過測試,因為您(大部分)記得當您添加功能、修復錯誤或重構時代碼中其他地方發生了什麼。 也就是說,我們不會總是使用小型應用程序,此外,隨著時間的推移,它們往往會變得更大、更複雜。 這使得手動測試變得困難並且超級煩人。

在我最近的幾個項目中,我被迫在沒有自動化測試的情況下工作,老實說,在代碼推送後讓客戶給我發電子郵件說應用程序在我什至沒有接觸過代碼的地方出現問題,這很尷尬。

因此,如果我的客戶既沒有預算也沒有打算添加任何自動化測試框架,我開始測試整個網站的基本功能,方法是向每個單獨的頁面發送 HTTP 請求,解析響應標頭並查找“200”回复。 這聽起來簡單明了,但是您可以做很多事情來確保保真度,而無需實際編寫任何測試、單元、功能或集成。

自動化測試

在 Web 開發中,自動化測試包括三種主要的測試類型:單元測試、功能測試和集成測試。 我們經常將單元測試與功能和集成測試結合起來,以確保一切作為一個整體應用程序順利運行。 當這些測試一致或按順序運行時(最好使用單個命令或單擊),我們開始稱它們為自動化測試,無論是否為單元。

這些測試(至少在 web 開發中)的主要目的是確保所有應用程序頁面都可以順利呈現,沒有致命(應用程序停止)錯誤或錯誤。

單元測試

單元測試是一個軟件開發過程,其中代碼的最小部分 - 單元 - 被獨立測試以確保正確運行。 這是 Ruby 中的一個示例:

 test “should return active users” do active_user = create(:user, active: true) non_active_user = create(:user, active: false) result = User.active assert_equal result, [active_user] end

功能測試

功能測試是一種用於檢查系統或軟件的特性和功能的技術,旨在涵蓋所有用戶交互場景,包括故障路徑和邊界情況。

注意:我們所有的例子都是用 Ruby 編寫的。

 test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end

集成測試

一旦模塊經過單元測試,它們就會被逐一集成,以檢查組合行為,並驗證需求是否正確實現。

 test "login and browse site" do # login via https https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:articles) end

理想世界中的測試

測試在行業中被廣泛接受並且是有意義的; 好的測試可以讓你:

  • 質量保證您的整個應用程序以最少的人力投入
  • 更容易識別錯誤,因為您確切地知道您的代碼在哪裡因測試失敗而中斷
  • 為您的代碼創建自動文檔
  • 避免“編碼便秘”,根據 Stack Overflow 上的一些傢伙的說法,這是一種幽默的說法,“當你不知道接下來要寫什麼,或者你面前有一項艱鉅的任務時,從小寫開始。”

我可以繼續談論測試有多棒,以及它們如何改變世界和 yada yada yada,但你明白了。 從概念上講,測試很棒。

相關:單元測試、如何編寫可測試代碼及其重要性

現實世界中的測試

雖然這三種類型的測試都有優點,但大多數項目都沒有編寫它們。 為什麼? 好吧,讓我分解一下:

時間/截止日期

每個人都有最後期限,編寫新的測試可能會妨礙完成一個。 編寫應用程序及其各自的測試可能需要時間半(或更多)時間。 現在,你們中的一些人不同意這一點,理由是最終節省了時間,但我認為情況並非如此,我將在“意見分歧”中解釋原因。

客戶問題

通常,客戶並不真正了解測試是什麼,或者為什麼它對應用程序有價值。 客戶往往更關心產品的快速交付,因此認為程序化測試會適得其反。

或者,它可能就像客戶沒有預算來支付實施這些測試所需的額外時間一樣簡單。

缺少知識

現實世界中有相當大的開發人員部落不知道測試的存在。 在每次會議、聚會、音樂會上(甚至在我的夢中),我都會遇到不知道如何編寫測試、不知道要測試什麼、不知道如何設置測試框架等的開發人員在。 學校並不完全教授測試,設置/學習框架以使其運行可能很麻煩。 所以是的,有一個明確的進入障礙。

'這是很多工作'

對於新手和有經驗的程序員來說,編寫測試可能會讓人不知所措,即使對於那些改變世界的天才類型也是如此,而且最重要的是,編寫測試並不令人興奮。 有人可能會想,“當我可以實現一項主要功能並取得令我的客戶印象深刻的結果時,我為什麼還要從事平淡無奇的忙碌工作呢?” 這是一個艱難的論點。

最後但並非最不重要的一點是,編寫測試很困難,而且計算機科學專業的學生也沒有為此受過訓練。

哦,使用單元測試進行重構並不好玩。

意見分歧

在我看來,單元測試對於算法邏輯是有意義的,但對於協調活代碼則沒有那麼重要。

人們聲稱,即使您在編寫測試之前投入了額外的時間,但在調試或更改代碼時可以為您節省數小時。 我不敢苟同並提出一個問題:您的代碼是靜態的,還是不斷變化的?

對於我們大多數人來說,它一直在變化。 如果您正在編寫成功的軟件,那麼您總是在添加功能、更改現有功能、刪除它們、吃掉它們,無論如何,為了適應這些變化,您必須不斷更改您的測試,而更改您的測試需要時間。

但是,你需要某種測試

沒有人會爭辯說缺乏任何類型的測試是最糟糕的情況。 對代碼進行更改後,您需要確認它確實有效。 很多程序員嘗試手動測試基礎:頁面是否在瀏覽器中呈現? 是否正在提交表單? 顯示的內容是否正確? 等等,但在我看來,這是野蠻的、低效的和勞動密集型的。

我用什麼代替

測試 Web 應用程序(無論是手動還是自動)的目的是確認任何給定頁面在用戶瀏覽器中呈現時沒有任何致命錯誤,並且它正確顯示其內容。 實現此目的的一種方法(在大多數情況下是一種更簡單的方法)是將 HTTP 請求發送到應用程序的端點並解析響應。 響應代碼告訴您頁面是否已成功傳遞。 通過解析 HTTP 請求的響應正文並蒐索特定的文本字符串匹配項,可以很容易地測試內容,或者,您可以更進一步並使用諸如 nokogiri 之類的網絡抓取庫。

如果某些端點需要用戶登錄,您可以使用為自動化交互設計的庫(在進行集成測試時非常理想),例如機械化登錄或單擊某些鏈接。 確實,在自動化測試的大局中,這看起來很像集成或功能測試(取決於您如何使用它們),但它的編寫速度要快得多,並且可以包含在現有項目中,或添加到新項目中,比建立整個測試框架更省力。 發現!

相關:聘請前 3% 的自由 QA 工程師。

在處理具有廣泛值的大型數據庫時,邊緣情況會帶來另一個問題。 測試我們的應用程序是否在所有預期數據集上順利運行可能會令人生畏。

解決它的一種方法是預測所有邊緣情況(這不僅困難,而且通常是不可能的)並為每個情況編寫測試。 這很容易變成數百行代碼(想像一下恐怖)並且維護起來很麻煩。 然而,通過 HTTP 請求和一行代碼,您可以直接在生產數據上測試這些邊緣案例,這些數據可以在本地下載到您的開發機器或登台服務器上。

當然,現在這種測試技術不是靈丹妙藥,並且有很多缺點,就像任何其他方法一樣,但我發現這些類型的測試更快,更容易編寫和修改。

實踐:使用 HTTP 請求進行測試

由於我們已經確定在沒有任何附帶測試的情況下編寫代碼不是一個好主意,所以我對整個應用程序的最基本的測試是在本地向其所有頁面發送 HTTP 請求並解析響應標頭以獲取200 (或所需)代碼。

例如,如果我們用 HTTP 請求代替(在 Ruby 中)編寫上述測試(尋找特定內容和致命錯誤的測試),它將是這樣的:

 # testing for fatal error http_code = `curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }` if http_code !~ /200/ return “articles_url returned with #{http_code} http code.” end # testing for content active_user = create(:user, name: “user1”, active: true) non_active_user = create(:user, name: “user2”, active: false) content = `curl #{Rails.application.routes.url_helpers.active_user_url(host: 'localhost', port: 3000) }` if content !~ /#{active_user.name}/ return “Content mismatch active user #{active_user.name} not found in text body” #You can customise message to your liking end if content =~ /#{non_active_user.name}/ return “Content mismatch non active user #{active_user.name} found in text body” #You can customise message to your liking end

curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }涵蓋了很多測試用例; 任何在文章頁面上引發錯誤的方法都會在此處捕獲,因此它在一次測試中有效地涵蓋了數百行代碼。

第二部分專門捕獲內容錯誤,可以多次使用以檢查頁面上的內容。 (可以使用mechanize處理更複雜的請求,但這超出了本博客的範圍。)

現在,如果您想測試特定頁面是否適用於大量不同的數據庫值(例如,您的文章頁面模板適用於生產數據庫中的所有文章),您可以執行以下操作:

 ids = Article.all.select { |post| `curl -s -o /dev/null -w “%{http_code}” #{Rails.application.routes.url_helpers.article_url(post, host: 'localhost', port: 3000) }`.to_i != 200).map(&:id) return ids

這將返回數據庫中所有未呈現的文章的 ID 數組,因此現在您可以手動轉到特定文章頁面並檢查問題。

現在,我知道這種測試方式在某些情況下可能不起作用,例如測試獨立腳本或發送電子郵件,並且不可否認它比單元測試慢,因為我們為每個測試直接調用端點,但是當您不能進行單元測試或功能測試,或兩者兼而有之,這總比沒有好。

您將如何構建這些測試? 對於小型、不復雜的項目,您可以將所有測試編寫在一個文件中,並在每次提交更改之前運行該文件,但大多數項目都需要一套測試。

我通常為每個端點編寫兩到三個測試,具體取決於我正在測試的內容。 您也可以嘗試測試單個內容(類似於單元測試),但我認為這將是多餘且緩慢的,因為您將為每個單元進行 HTTP 調用。 但是,另一方面,它們會更乾淨,更容易理解。

我建議將您的測試放在您的常規測試文件夾中,每個主要端點都有自己的文件(例如,在 Rails 中,每個模型/控制器都有一個文件),這個文件可以根據我們的內容分為三個部分正在測試。 我經常至少有三個測試:

測試一

檢查頁面返回時是否沒有任何致命錯誤。

測試一檢查頁面返回時沒有任何致命錯誤。

請注意我是如何為Post列出所有端點的列表並對其進行迭代以檢查每個頁面是否在沒有任何錯誤的情況下呈現。 假設一切順利,並且所有頁面都已渲染,您將在終端中看到如下內容: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []

如果任何頁面未呈現,您將看到如下內容(在此示例中, posts/index page有錯誤,因此未呈現): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- [{:url=>”posts_url”, :params=>[], :method=>”GET”, :http_code=>”500”}]

測試二

確認所有預期的內容都在那裡:

測試二確認所有預期的內容都在那裡。

如果在頁面上找到了我們期望的所有內容,結果如下所示(在此示例中,我們確保posts/:id具有帖子標題、描述和狀態): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- []

如果在頁面上找不到任何預期的內容(這裡我們希望頁面顯示帖子的狀態 - 如果帖子處於活動狀態,則顯示“活動”,如果帖子被禁用,則顯示“禁用”)結果如下所示: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- [“Active”]

測試三

檢查頁面是否在所有數據集(如果有)中呈現:

測試 3 檢查頁面是否在所有數據集中呈現。

如果所有頁面都渲染沒有任何錯誤,我們將得到一個空列表: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []

如果某些記錄的內容呈現問題(在此示例中,ID 為 2 和 5 的頁面出現錯誤),結果如下所示: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]

如果你想擺弄上面的演示代碼,這裡是我的 github 項目。

那麼哪個更好? 這取決於…

如果出現以下情況,HTTP 請求測試可能是您最好的選擇:

  • 您正在使用 Web 應用程序
  • 你正處於時間緊縮狀態,想快速寫點東西
  • 您正在處理一個未編寫測試的大型項目,預先存在的項目,但您仍需要某種方法來檢查代碼
  • 您的代碼涉及簡單的請求和響應
  • 您不想花費大部分時間來維護測試(我在某處讀過單元測試 = 維護地獄,我部分同意他/她的觀點)
  • 您想測試應用程序是否適用於現有數據庫中的所有值

在以下情況下,傳統測試是理想的:

  • 您正在處理的不是 Web 應用程序,例如腳本
  • 你正在編寫複雜的算法代碼
  • 你有時間和預算來寫測試
  • 業務需要無錯誤或低錯誤率(財務、龐大的用戶群)

感謝您閱讀文章; 您現在應該有一種可以默認使用的測試方法,當您時間緊迫時可以依靠它。

有關的:
  • 性能和效率:使用 HTTP/3
  • 保持加密,保持安全:使用 ESNI、DoH 和 DoT