讓我們深入了解賽普拉斯的端到端測試

已發表: 2022-03-10
快速總結↬端到端測試對您來說是一個痛苦的話題嗎? 在本文中,Ramona Schwering 解釋瞭如何使用 Cypress 處理端到端測試,並使其對您自己來說不再那麼乏味和昂貴,而是變得有趣。

今天很難想像沒有自動化測試的軟件開發。 多種不同的測試程序將確保高質量。 作為測試的基礎,我們可以使用許多單元測試。 最重要的是,在金字塔的中間,可以說是集成測試。 端到端測試位於最頂端,涵蓋最關鍵的用例。 這第三種測試將是本文的重點。

但是,端到端測試確實存在一些令人擔憂的缺陷

  • 端到端測試很慢,因此在每個持續集成和持續部署 (CI/CD) 策略中都構成了重大障礙。 不僅如此,想像一下完成一項任務、一項功能或任何其他實現——等待測試執行會耗盡每個人的耐心。
  • 由於調試工作,這種端到端測試難以維護、容易出錯並且在各個方面都非常昂貴。 各種因素都可能導致這種情況。 你的測試應該感覺像一個助手,而不是一個障礙。
  • 開發人員最大的噩夢是不穩定的測試,這是一種以相同方式執行但導致不同結果的測試。 它就像一個“Heisenbug”,只有在你不測量正在測試的應用程序時才會出現——也就是說,如果你不看它。
海森蟲
Heisenfails 是真實的,類似於 Heisenbugs。 (大預覽)

但不要擔心:您不必屈服於這些陷阱。 讓我們看看如何防止其中的許多。 但是,我不會只承諾月亮而不交付。 在本指南中,我們將一起編寫一些測試,我已在 GitHub 存儲庫中為您公開了這些測試。 通過這種方式,我希望向您展示端到端測試可以很有趣! 讓我們開始吧。

什麼是端到端測試?

在談論端到端(或 E2E)測試時,我喜歡將其稱為“基於工作流的”。 這句話很好地總結了端到端測試:它模擬實際的用戶工作流程,並且應該包括盡可能多的功能區域和應用程序中使用的技術堆棧的一部分。 最後,計算機會偽裝成客戶,並試圖表現得像真正的用戶。 這些測試最適合對應用程序的整個系統施加恆定壓力,因此,當整個應用程序堆棧存在時,它們是確保質量的重要措施

機器人
端到端測試由模擬真實用戶的計算機執行。 (大預覽)

讓我們回想一下我們想要通過所有這些實現的目標。 我們知道,前端測試是一組測試 Web 應用程序 UI 的實踐,包括它的功能。 有道理——通過這些措施,我們可以確保我們的應用程序正常工作,並且未來的任何更改都不會破壞我們的代碼。 為了有效地實現這一點,您可能想知道您需要測試什麼以及需要測試多少。

這是一個有效的問題。 您可能會在一個比喻中找到一個可能的答案:測試自動化金字塔,首先由 Mike Cohn 引入,並由 Martin Fowler 進一步說明,它展示瞭如何使測試高效。 我們在金字塔的最低層發現快速且廉價的單元測試,而在頂部發現耗時且昂貴的 UI 測試(端到端測試)。

測試金字塔
自動化測試金字塔。 (大預覽)

解釋這一點及其優缺點對於它自己的文章來說就足夠了。 我想專注於一個層面。 如果有效地優先考慮,端到端測試尤其可以顯著提高質量。 這樣做,我們可以不斷地使我們的系統處於壓力之下,並確保我們的應用程序的主要功能正常工作。

我的賽普拉斯之旅

當我開始學習如何編寫端到端測試時,我在 Behat(一個面向場景的行為驅動開發 (BDD) 框架)之上使用了 Mink(一個 PHP 庫)。 我開始使用 Selenium,它具有所有優點和缺點。 因為我的團隊已經開始大量使用 Vue.js,所以我們改為使用基於 JavaScript 的測試框架,以確保完美的集成和兼容性。 我們當時的選擇是 Nightwatch.js,所以我從頭開始構建了我們的新測試套件。

在此期間,我們經常偶然發現兼容性問題。 你可以稱之為依賴地獄——更不用說我們在 Selenium 和後來的 WebDriver 中看到的所有限制。

  • 在我們的團隊中,我們無法確定 CI 的 Chrome 版本。 因此,如果發布了 Chrome 的更新,Nightwatch.js 的速度不夠快,無法兼容,導致我們的測試管道出現許多故障。
  • 由於 Nightwatch.js 的等待可能性與我們的產品沒有最佳匹配,導致不穩定測試的測試端原因的數量開始上升。

因此,我們開始考慮重新構建我們的測試套件。 在參觀了一次非會議後,我發現了賽普拉斯。

Cypress 是一個不使用 Selenium 或 WebDriver 的一體化測試框架。 該工具使用 Node.js 在特殊控制下啟動瀏覽器。 該框架中的測試在瀏覽器級別運行,而不僅僅是遠程控制。 這提供了幾個優點。

簡而言之,我選擇這個框架的原因如下:

  • 出色的調試能力
    Cypress 的測試運行器可以通過快照跳回到應用程序的任何狀態。 因此,我們可以直接看到錯誤及其之前的所有步驟。 此外,還有對 Chrome 的開發者工具 (DevTools) 的完全訪問權限,並且點擊次數被完全記錄。
  • 在測試或 UI 或 API 響應中等待操作的更好方法
    Cypress 帶來了隱式等待,因此不需要進行適當的檢查。 您還可以讓測試等待動畫和 API 響應。
  • 測試是用 JavaScript 編寫的
    這減輕了編寫測試的學習曲線。 Cypress 的測試運行程序是開源的,因此符合我們的產品策略。

然而,這篇文章是一個指南,所以讓我們停下來了解這些一般信息並開始吧。

入門

安裝並啟動賽普拉斯

讓我們從頭開始。 在談論賽普拉斯時,我通常首先通過mkdir創建一個新目錄,然後立即安裝賽普拉斯。 最簡單的安裝方法如下圖所示:

賽普拉斯使用 Node.js
Cypress 使用 Node.js(大預覽)

一點提示:如果不想使用 npm,可以通過 Yarn 安裝 Cypress:

 yarn add cypress --dev

另一種方法是直接下載,使用賽普拉斯提供的 ZIP 文件夾。 而已! 安裝完成後,您就可以開始了。

有兩種方法可以開始運行賽普拉斯測試。 第一種是在控制台中啟動 Cypress,然後無頭運行您的測試:

 ./node_modules/.bin/cypress run

第二種方法是使用 Cypress 的一項簡潔功能,即其集成的測試運行程序。 測試運行器是用於運行測試的 UI。 要啟動它,您可以使用類似的命令:

 ./node_modules/.bin/cypress open

此命令將打開測試運行器。 當你第一次打開 Cypress 時,你會看到這個界面:

賽普拉斯的測試運行器
賽普拉斯的測試跑步者一見鍾情。 (大預覽)

賽普拉斯提供了一些預先編寫的示例測試來展示其功能並為您提供一些起點——這就是提供測試的原因。 讓我們暫時忽略這些,因為我們想盡快編寫自己的。 但是,請記住這個“集成測試”區域,因為它會解釋以後會發生的很多魔法。

賽普拉斯結構的第一印象

現在是時候在選擇的集成開發環境 (IDE) 中打開我們新創建的項目了。 如果您導航到此文件夾,您將看到以下測試結構:

 smashing-example └── cypress └── fixtures └── integration └── plugins └── support └── cypress.json

讓我們看看這些文件夾:

  • fixtures
    在這裡您可以找到與其他實體無關的固定測試數據。 因此,這裡沒有存儲 ID,可以根據本地狀態更改。
  • integration
    您將在此處找到實際測試。
  • plugins
    在這裡,您可以使用現有的賽普拉斯插件或您自己的插件來擴展賽普拉斯。
  • support
    在這裡,您可以擴展賽普拉斯本身。 您自己的命令和助手位於此處。
  • cypress.json
    在此處修改配置,包括環境。

好吧,我想我們現在可以在 Cypress 上找到我們的方法了,無論是測試運行程序還是源代碼。 但是我們如何開始呢? 我們要測試什麼?

選擇一個測試用例

典型的端到端測試可能會變得複雜,尤其是在有很多步驟的情況下。 手動執行會花費很多時間。 由於這種複雜性,E2E 測試難以實現自動化並且運行緩慢。 因此,我們需要仔細決定哪些案例要自動化。

在我看來, “基於工作流”這個詞很關鍵:我們會根據典型的用戶故事來選擇測試用例。 但是,由於運行時間的原因,不建議涵蓋每個可用的工作流程。 因此,我們需要一種方法來確定測試用例的優先級。

在我的團隊中,我們的項目有幾個標準。 測試用例應該:

  • 涵蓋一個特性的最通用和最常用的工作流程,例如 CRUD 操作(術語“快樂路徑”很好地描述了這些工作流程);
  • 使用風險分析,涵蓋最易受攻擊的 E2E 測試工作流程(即錯誤會造成最大損害的地方);
  • 避免重複覆蓋;
  • 如果單元測試更合適,則不一定使用(使用 E2E 測試來測試您的軟件對錯誤的響應,而不是錯誤本身)。

要記住的第二件最重要的事情是只測試您明確想要測試的工作流程。 使您的測試工作所需的所有其他步驟應在測試之外使用 API 操作來完成,以避免對其進行測試。 這樣,您將確保最短的測試運行時間,並在測試用例失敗時獲得清晰的結果。 將此工作流程視為最終用戶會:專注於使用功能而不是技術實現

例子:

如果您想在在線商店中測試結帳流程,請不要執行所有其他步驟,例如創建產品和類別,即使您需要它們來處理結帳。 例如,使用 API 或數據庫轉儲來製作這些東西,並僅為結帳配置測試。

示例:在 Smashing Magazine 中查找我的文章

我想為這個網站寫一個測試,Smashing Magazine。 我不能保證這個測試永遠是最新的,但我們希望它會持續下去。 無論哪種方式,您都可以在 GitHub 存儲庫中找到此示例。

創建我們的第一個 Cypress 測試

integration文件夾中,我們將首先創建一個新文件。 我們稱之為find-author.spec.js 。 後綴.spec代表“規範”。 就測試而言,這是指您的應用程序必須滿足的給定功能或應用程序的技術細節。

要將這個空的 JavaScript 文件變成測試的主目錄,我們首先要為測試套件提供其結構。 我們將使用名為describe的方法。 describe()context()用於包含和組織測試。 換句話說,這個方法作為我們測試的框架。 因此,我們的測試文件將如下所示:

 // find-author.spec.js describe('Find authors at smashing', () => { //... });

下一步是創建實際測試。 我們將使用it的方法。 it()specify()用於表示實際測試。 如您所見,我們可以在一個文件中捕獲多個測試,從而提供一些出色的結構化選項。

 // find-author.spec.js describe('Find authors at smashing', () => { it('Find the author Ramona Schwering', () => { cy.log('This is our brand-new test'); }); });

小提示如果您熟悉 Mocha,您可能已經註意到一些相似之處。 Cypress 建立在 Mocha 之上,因此語法是相同的。

好吧,讓我們繼續。 如果我們在 Cypress 的測試運行器中運行我們的測試,我們會注意到 Cypress 將打開一個瀏覽器來運行測試。 該瀏覽器如下面的截圖所示:

執行我們的第一個測試
我們的第一個測試,在 Cypress 的測試運行器中執行。 (大預覽)

恭喜! 我們已經編寫了第一個測試! 當然,它沒有多大作用。 我們需要繼續。 讓我們用生活來填滿我們的考驗。

用生命填滿考驗

測試網站時首先要做的是什麼? 對,我們需要打開網站。 我們可以使用 Cypress 命令來做到這一點。 命令是什麼,你可能想知道?

使用命令

端到端測試中主要使用兩種類型的指令。 第一種指令,命令,代表測試中的各個步驟。 在賽普拉斯的上下文中,命令是賽普拉斯與您的網站交互所做的一切。 這種交互可以是任何東西——點擊、向下滾動網站,甚至是找到一個元素。 因此,命令將是我們用來填充測試的重要內容之一。

因此,我們的第一個命令將是導航到該網站的命令smashingmagazine.com 。 此命令稱為visit

使用它,我們的測試將如下所示:

 // find-author.spec.js describe('Find authors at smashing', () => { it('Find the author Ramona Schwering', () => { cy.visit('https://www.smashingmagazine.com/'); }); });

我經常使用一個命令——你也會。 它被稱為get

 cy.get('selector');

該命令根據其選擇器返回一個元素——類似於 jQuery 的$(…) 。 因此,您將使用此命令來查找要與之交互的部分。 通常,您會使用它來啟動一系列命令。 但是等等——命令鍊是什麼意思?

正如本文開頭所提到的,所有測試以及與之相關的所有其他內容都是用 JavaScript 編寫的。 您可以將測試(即語句)中的命令放在一個鏈中(換句話說,鍊式)。 這意味著命令可以將命令的主題(或返回值)傳遞給以下命令,正如我們從許多測試框架中所知道的那樣。

好的,我們將使用get命令啟動一系列命令。 要使用get找到一個元素,我們需要先找到它的選擇器。 找到一個唯一的選擇器是必不可少的,否則賽普拉斯會返回所有匹配的元素; 所以,請記住這一點,如果它是無意的,請避免它。

與元素交互

Cypress 本身有一個功能可以幫助您找到要使用的元素的選擇器。 此功能稱為 Selector Playground,它可以幫助您發現組件的唯一選擇器或查看選擇器或文本字符串的所有匹配元素。 因此,此功能可以在此任務中為您提供很多幫助。 要啟用它,只需單擊測試 UI 標題中的十字準線圖標,然後將鼠標懸停在所需元素上:

使用選擇器遊樂場
使用 Selector Playground 來識別唯一的選擇器。 (大預覽)

如上面的屏幕截圖所示,工具提示將在懸停時顯示選擇器,或者在單擊元素時出現的十字線圖標下方的這個小欄中顯示選擇器。 在這個欄中,您還可以看到有多少元素會匹配給定的選擇器——在我們的例子中確保它的唯一性。

有時,那些自動生成的選擇器可能不是您想要使用的選擇器(例如,如果它們很長或難以閱讀或不滿足您的其他條件)。 在我看來,下面生成的選擇器很難理解並且太長:

不是一個理想的選擇器
此選擇器可能不適合使用。 (大預覽)

在這種情況下,我會回退到瀏覽器的 DevTools 來找到我獨特的選擇器。 您可能熟悉這些工具; 就我而言,我經常為此選擇 Chrome。 但是,其他受支持的瀏覽器可能會提供類似的功能。 這個過程感覺類似於 Selector Playground,除了我們在“元素”選項卡中使用 DevTools 的功能。

通過開發者工具查找選擇器
使用瀏覽器的開發工具來查找唯一的選擇器。 (大預覽)

為確保選擇器是唯一的,我建議您在 DevTools 的代碼視圖中搜索它。 如果您只找到一個結果,您可以確信它是獨一無二的。

您知道有許多不同的選擇器類型嗎? 根據種類的不同,測試的外觀甚至行為都可能完全不同。 有些品種比其他品種更適合端到端測試。 如果您想知道使用哪些選擇器來保持測試穩定和乾淨,我可以為您指出我的一篇文章,其中涵蓋了這個問題。 賽普拉斯的開發人員自己在他們的最佳實踐中就該主題提供了一些指導。

我們的測試作為一系列命令

好的,回到我們的測試。 在其中,我們要顯示我們的工作流程:

“作為用戶,我將搜索作者的文章,並通過其中一篇文章中的參考區域導航到作者的網站。”

我們將重現用戶使用命令執行的步驟。 我將在完成的測試下方粘貼註釋,其中將解釋步驟:

 // find-author.spec.js it('Find the author Ramona Schwering', () => { // Open the website cy.visit('https://www.smashingmagazine.com'); // Enter author's name in search field cy.get('#js-search-input').type('Ramona Schwering'); // Navigate to author's article cy.get('h2 > a').first().click(); // Open the author's page cy.get('.author-post__author-title').click(); });

此示例處理我們要測試的工作流。 賽普拉斯將執行此測試。 那麼,是時候說“恭喜”了嗎? 我們終於寫完了第一個測試嗎?

好吧,請仔細看看。 賽普拉斯將執行它,但它只會執行測試告訴它的操作,即您編寫的任何內容。 如果你在測試運行器中運行它,你可以看到它是否通過了——但如果你無頭運行它就不行。 通過這個測試,我們只知道 Cypress 是否可以成功運行我們的命令,而不知道我們是否最終出現在作者的網站上。 所以,我們需要教我們的測試來確定這一點。

使用斷言

第二種類型的語句負責對 UI 所需狀態的描述——即,某物是否應該存在、可見或不再可見。 Cypress 中的斷言基於 Chai 和 Sinon-Chai 斷言,這在語法中很明顯。

請記住,我們想檢查我們是否在作者的個人資料頁面上——在這個例子中是我的。 因此,我們需要為此添加一個斷言:

 // find-author.spec.js it('Find the author Ramona Schwering', () => { // Open the website cy.visit('https://www.smashingmagazine.com'); // Enter author's name in search field cy.get('#js-search-input').type('Ramona Schwering'); // Navigate to author's article cy.get('h2 > a').first().click(); // Open the author's page cy.get('.author-post__author-title').click(); // Check if we're on the author's site cy.contains('.author__title', 'Ramona Schwering').should('be.visible'); });

好的,現在我們已經編寫了一個有價值的測試。 所以,是的,恭喜你編寫了你的第一個測試......即使它還不完美。

讓我們的測試漂亮

即使我們已經成功編寫了第一個有意義的測試並在此過程中學習了核心概念,如果它是在拉取請求中提出的,我也不會合併它。 有幾件事要做才能讓它發光。

慢慢來

賽普拉斯幾乎在每個命令中都有一個內置的重試選項,因此您不必等待查看某個元素是否已經存在。 但是,這只是查看 DOM 中是否存在元素,僅此而已。 賽普拉斯無法預測您的應用程序所做的一切,因此如果您僅依賴於此,可能會出現一些問題。

端到端測試的時間
時間是端到端測試中的一個重要方面。 (大預覽)

如果用戶想查看仍在加載的網站,他們會怎麼做? 他們很可能會等到網站的某些部分變得可見(因此,加載),然後與他們進行交互。 在我們的測試中,我們想要精確地模仿:我們想要在開始交互之前等待 UI 的變化。 在大多數情況下,我們會將這種行為限制在我們需要的元素上,因此對這些元素使用斷言。

如您所見,我們必須讓我們的測試多次等待。 但是,等待太多次也不好。 根據經驗,我建議使用斷言來檢查要與之交互的元素是否已完全加載,作為確定正在測試的網站是否已加載的第一步。

讓我們以我們測試的這樣一個部分為例。 我添加了一個斷言以確保我們的頁面已完全加載

 // find-author-assertions.spec.js // Open website cy.visit('https://www.smashingmagazine.com'); // Ensure site is fully loaded cy.get('.headline-content').should('be.visible'); // Enter author's name in the search field cy.get('#js-search-input').type('Ramona Schwering');

繼續以這種方式向我們的網站將有加載時間或需要重新呈現的幾個元素的所有實例添加斷言。 完整的測試文件請查看GitHub倉庫中對應的測試。

為了避免落入不穩定測試的陷阱,我想給你最後一個提示:永遠不要使用固定的等待時間,例如cy.wait(500)等。

API 響應是你的朋友

我特別喜歡在測試中使用一種巧妙的等待可能性。 在 Cypress 中,可以使用網絡功能——在應用程序中等待的另一種有用方法是使用這些功能來處理網絡請求。 這樣,您可以讓測試等待成功的 API 響應。

如果我們以我們的工作流程為例,一個步驟可以充分利用 API 等待的可能性。 我在考慮搜索。 相應的用戶故事可能如下:

“作為開發人員,我希望確保我們的搜索結果已經完全加載,這樣舊結果的文章就不會誤導我們的測試。”

讓我們把它應用到我們的測試中。 首先,我們需要定義稍後要等待的路由。 我們可以為此使用intercept命令。 我會搜索請求,帶來我需要的數據——在這種情況下是搜索結果。

為了使這個示例簡單,我將使用通配符作為 URL。 之後,我將使用別名,以便 Cypress 稍後可以使用此路由。

 // find-author-hooks.spec.js // Set the route to work with it('Find the author Ramona Schwering', () => { // Route to wait for later cy.intercept({ url: '*/indexes/smashingmagazine/*', method: 'POST' }).as('search'); // With this alias Cypress will find the request again //...

在賽普拉斯中,所有定義的路由都在測試開始時顯示。 所以,我也想把這些intercept命令放在我測試的開始。

Cypress 測試運行程序中的 API 路由
API 路由將顯示在我們測試的頂部。 (大預覽)

現在,我們可以在斷言中使用這個路由別名。 最簡單的方法是使用 Cypress 的wait命令,直接使用前面提到的別名。 但是,單獨使用此命令將導致無論結果如何都等待響應。 即使是 400 或 500 之類的錯誤代碼也會被視為通過,而您的應用程序很可能會中斷。 所以我建議添加另一個這樣的斷言:

 // find-author-hooks.spec.js // Later: Assertion of the search request's status code cy.wait('@search') .its('response.statusCode').should('equal', 200);
API 響應的斷言
對 API 響應的斷言,如賽普拉斯的測試運行器中所示。 (大預覽)

這樣,我們可以精確地等待軟件的數據、更改等,而不會在應用程序壓力過大時浪費時間或陷入問題。 同樣,您可以在我的 GitHub 存儲庫中找到完整的示例文件。

配置賽普拉斯

我遺漏了一個小細節。 如果您仔細查看完整的測試示例,它與我們在本指南中使用的示例略有不同。

 // Cypress describe('Find author at smashing', () => { beforeEach(() => { // Open website cy.visit('https://www.smashingmagazine.com'); }); //...

我只用斜線打開 Smashing Magazine 的網站。 這是如何運作的? 好吧,像這樣使用這個命令將導航到我們測試的baseUrlbaseUrl是一個配置值,可用作cy.visit()cy.request()命令 URL 的前綴。 在其他值中,我們可以在cypress.json文件中定義這個值。 對於我們的測試,我們將這樣設置baseUrl

 // cypress.json { "baseUrl": "https://www.smashingmagazine.com" }

榮譽獎:鉤子

我還想提一個話題,即使我們的示例測試不適合使用它。 正如在其他測試框架中常見的那樣,我們可以通過所謂的生命週期掛鉤來定義測試之前和之後發生的事情。 更準確地說,這些存在是為了在一個或所有測試之前或之後執行代碼:

 // Cypress describe('Hooks', function() { before(() => { // Runs once before all tests }); after(() => { // Runs once after all tests }); beforeEach(() => { // Runs before each test }); afterEach(() => { // Runs after each test }); });

我們想用不止一個測試填充我們的測試文件,所以我們應該尋找我們想要在它們之前或之後執行的常見步驟。 我們的第一行就是一個很好的例子,即visit命令。 假設我們想在每個測試之前打開這個網站,我們示例中的beforeEach掛鉤將如下所示:

 // Cypress describe('Find author at smashing', () => { beforeEach(() => { // Open website cy.visit('https://www.smashingmagazine.com'); }); //... 
在每個鉤子之前
beforeEach鉤子顯示在測試運行器的日誌中。 (大預覽)

例如,我在日常工作中經常使用它來確保我的應用程序在測試之前重置為其默認狀態,從而將測試與其他測試隔離開來。 (永遠不要依賴以前的測試! )彼此隔離運行測試以保持對應用程序狀態的控制。

每個測試都應該能夠獨立運行——獨立於其他測試。 這對於確保有效的測試結果至關重要。 有關這方面的詳細信息,請參閱我最近的一篇文章中的“我們曾經共享的數據”部分。 現在,如果您想查看整個測試,請參閱 GitHub 上的完整示例。

結論

在我看來,端到端測試是 CI 的重要組成部分,可以將應用程序的質量保持在高水平,同時減輕測試人員的工作量。 Cypress 是我首選的工具,用於快速、穩定、高效地調試端到端測試,並將它們作為 CI 的一部分與任何拉取請求並行運行。 如果您已經熟悉 JavaScript,那麼學習曲線會很平緩。

我希望我能夠為您提供一些指導,並為您提供編寫 Cypress 測試的起點和一些實用的入門技巧。 當然,所有代碼示例都可以在 GitHub 存儲庫中找到,因此請隨意查看。

當然,這只是一個起點; 關於賽普拉斯測試,還有很多東西要學習和討論——我會給你一些關於接下來要學習什麼的建議。 考慮到這一點,祝測試愉快!

資源

  • 原始的粉碎示例,Ramona Schwering
    本文中示例的 GitHub 存儲庫。
  • 賽普拉斯文檔
  • “食譜”,賽普拉斯
    精選示例、食譜和課程。
  • “學習使用 JavaScript 編碼:賽普拉斯”(課程),CodeLikeThis
  • 編寫端到端測試的最佳實踐”,Shopware Docs