如何在測試環境中實現 API 模擬呼叫

幂简集成發表於2024-09-11

自動化 API 測試作為保障軟體質量的有效途徑,可助你深入理解目標行為及展示問題原因。尤其在 API 為行業普及的背景下,熟練掌握實現 API 單元測試以及索求優質 API 測驗人選之技可能稍顯複雜。

然而,無論是 API 開發階段,抑或是新增功能開發過程,系統性地測試預設行為均能節約大量時間,更易發現問題。透過構建模擬 API 呼叫,便可運用寶貴的單元測試資源,規避實時 API 呼叫所帶來的困擾。

Node.js 為行為驅動 JavaScript 提供多種自動化測試環境,其中 Jest 和 Jasmine 兩大框架廣受歡迎。本文將對其進行對比分析。首先,探討行為驅動開發中運用 API 的最佳實踐與技巧;其次,講解如何在 Jasmine 或 Jest 中設定並執行 API 測試;最後,對兩框架進行對比,協助你選擇最符合需求的工具。

為什麼要模擬 API 呼叫?

也許您在讀這篇文章時會想:測試很棒,但為什麼不測試實際的 API 呢?您可以使用 Axios 之類的庫從網路發出 HTTP 請求。如果您正在開發 API,它可以幫助您在部署之前在本地進行規劃。如果您的 API 已經部署,但您正在新增新功能,那麼您肯定不想將未經測試的程式碼推送到實時版本。此外,您可能想要測試一個因過度使用而收費或具有不確定結果(例如來自資料庫的動態資料)的 API。模擬 API 呼叫可讓您在這些情況下進行控制,並加快後續開發速度。行為驅動開發的第一步是列出您的 API 在正常執行時應該做的所有事情。每一項都應該具體、可衡量且確定。如果您有寫得很好的文件,這可能是您的起點。如果您剛剛開始,那麼這個初步計劃可能會變成您的文件。

對於每個步驟,列出一個示例,說明此請求的原始資料是什麼樣子以及響應應該是什麼樣子。例如:

  • 請求時返回帶有使用者帖子的 200 狀態
  • 請求:{method:’get’,body:{user:“Jack”}}
  • 回覆:{status:200,posts:posts:[“我剛買了一些魔豆!”]}

為了在不連線網路的情況下測試這些行為,您應該將此邏輯與主要路由分開。這樣,您就可以在測試中輕鬆匯入 API 所依賴的相同函式。您可以建立一個函式來處理請求物件並返回一個承諾來模擬非同步模擬 API 呼叫。它可能看起來像這樣:

<strong>function</strong> <strong>simulateAsyncCall</strong>(request) {<br>
  <strong>return</strong> <strong>new</strong> <strong>Promise</strong>((resolve, reject) =&gt; {<br>
    setTimeout(() =&gt; {<br>
      <strong>switch</strong> (request.method) {<br>
        <strong>case</strong> 'get':<br>
          <strong>const</strong> user = <strong>getUser</strong>(request);<br>
          <strong>if</strong> (user) {<br>
            <strong>resolve</strong>({ status: 200, posts: user.posts });<br>
          } <strong>else</strong> {<br>
            <strong>resolve</strong>({ status: 404, message: 'Not Found' });<br>
          }<br>
          <strong>break</strong>;<br>
        <strong>case</strong> 'post':<br>
          <strong>if</strong> (<strong>passwordIsValid</strong>(request)) {<br>
            <strong>addToPosts</strong>(request);<br>
            <strong>resolve</strong>({ status: 200, message: 'Added Post' });<br>
          } <strong>else</strong> {<br>
            <strong>resolve</strong>({ status: 401, message: 'Unauthorized' });<br>
          }<br>
          <strong>break</strong>;<br>
        default:<br>
          <strong>resolve</strong>({ status: 400, message: 'Bad Request' });<br>
      }<br>
    }, 300);<br>
  });<br>
}

如何使用 Jasmine 建立 Mock API?

在安裝任何 Node 模組之前,你應該確保你的根目錄中有一個 package.json 檔案。如果沒有,你可以使用以下命令進行設定:npm init -y

讓我們首先在你的專案目錄中安裝 Jasmine:npm install jasmine –save-dev

完成後,您可以使用以下命令配置 Jasmine:node node_modules/jasmine/bin/jasmine.js init

這將建立一個 spec 資料夾,其中包含一些具有預設設定的配置檔案。請務必記住,我們編寫的測試檔案應以 “spec.js” 結尾

最後,我們需要在 package.json 檔案中設定測試命令。開啟此檔案並向物件新增一個包含 Jasmine 模組路徑的test鍵。它應該如下所示:scripts

"scripts":{<br>
  "test":"jasmine"<br>
}

讓我們確保所有設定都正確。在spec名為my-first-spec.jspaste this 的資料夾中建立一個名為的檔案:

<strong>describe</strong>('My Jasmine Setup', <strong>function</strong> () {<br>
  <strong>var</strong> a = true;<br>
<br>
  <strong>it</strong>('tests if the value of a is true', <strong>function</strong> () {<br>
    <strong>expect</strong>(a).<strong>toBe</strong>(true);<br>
  });<br>
});

您可以透過執行我們在 package.json 中放入的測試指令碼在終端中執行測試:npm run test

您的測試應該會透過!嘗試將值更改a為 false,然後再次執行它,看看測試失敗時會是什麼樣子。請注意,當出現問題時,它會向您提供描述性訊息。

Jasmine 測試套件的另一個很酷的功能是 “beforeEach” 和 “afterEach” 函式。這允許您在每個 “it” 塊之前或之後執行某些操作。讓我們在每次測試之前建立一個新的 MockAPI 類例項。

以下指令碼可以在 Jasmine 或 Jest 中執行:

<strong>const</strong> <strong>MockAPI</strong>= require('../MockAPI.js');<br>
<br>
<strong>describe</strong>("Mock API",()=&gt;{<br>
  <strong>let</strong> mockAPI;<br>
  <strong>let</strong> mockDatabase=<br>
  {<br>
    users:[<br>
    {<br>
      name:"Jack",<br>
      passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123",<br>
      posts:["I just bought some magic beans!"]<br>
    },<br>
    {<br>
      name:"Jill",<br>
     passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"<br>
      posts:["Jack fell down!"]<br>
    },<br>
    ]<br>
  };<br>
<br>
  <strong>beforeEach</strong>(()=&gt;{<br>
    mockAPI= <strong>new</strong> <strong>MockAPI</strong>(mockDatabase)<br>
  })<br>
<br>
  <strong>it</strong>("returns a 400 bad request status if the request is invalid",()=&gt;{<br>
    <strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>({})<br>
    <strong>return</strong> mockApiCall.<strong>then</strong>(response=&gt;{<br>
      <strong>expect</strong>(response.status).<strong>toBe</strong>(400)<br>
    })<br>
  })<br>
<br>
  <strong>describe</strong>("get requests",()=&gt;{<br>
    <strong>const</strong> validRequest={method:'get',body:{user:"Jack"}};<br>
    <strong>const</strong> invalidRequest={method:'get',body:{user:"Tod"}};<br>
<br>
    <strong>it</strong>("returns a 404 status if a user is not found",()=&gt;{<br>
      <strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(invalidRequest)<br>
      <strong>return</strong> mockApiCall.<strong>then</strong>(response=&gt;{<br>
      <strong>expect</strong>(response.status).<strong>toBe</strong>(404)<br>
      })<br>
    });<br>
<br>
    <strong>it</strong>("returns a 200 status with a user's posts",()=&gt;{<br>
      <strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(validRequest)<br>
      <strong>return</strong> mockApiCall.<strong>then</strong>(response=&gt;{<br>
      <strong>expect</strong>(response.status).<strong>toBe</strong>(200)<br>
      <strong>expect</strong>(response.posts).<strong>toEqual</strong>(["I just bought some magic beans!"])<br>
      })<br>
    });<br>
  })<br>
<br>
  <strong>describe</strong>("post requests",()=&gt;{<br>
    <strong>const</strong> validRequest={method:'post',body:{user:"Jill",password:'hill',post:"He broke his crown!"}}<br>
    <strong>const</strong> invalidRequest={method:'post',body:{user:"Jill",password:'beanstock',post:"Jack is cool..."}}<br>
<br>
    <strong>it</strong>("returns a 401 unauthorized status if the wrong credentials are sent",()=&gt;{<br>
      <strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(invalidRequest)<br>
      <strong>return</strong> mockApiCall.<strong>then</strong>(response=&gt;{<br>
        <strong>expect</strong>(response.status).<strong>toBe</strong>(401)<br>
        <strong>expect</strong>(mockAPI.db).<strong>toEqual</strong>(mockDatabase)<br>
      })<br>
    })<br>
<br>
    <strong>it</strong>("returns a 200 status and adds the post to the database",()=&gt;{<br>
     <strong>const</strong> newDatabase={<br>
     users:[<br>
      {<br>
        name:"Jack",<br>
        passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"<br>
        posts:["I just bought some magic beans!"]<br>
      },<br>
      {<br>
        name:"Jill",<br>
        passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"<br>
        posts:["Jack fell down!","He broke his crown!"]<br>
      },<br>
     ]<br>
     }<br>
     <strong>const</strong> mockApiCall=mockAPI.<strong>simulateAsyncCall</strong>(validRequest)<br>
     <strong>return</strong> mockApiCall.<strong>then</strong>(response=&gt;{<br>
     <strong>expect</strong>(response.status).<strong>toBe</strong>(200)<br>
     <strong>expect</strong>(mockAPI.db).<strong>toEqual</strong>(newDatabase)<br>
     })<br>
    })<br>
  })<br>
})

如何使用 Jest 模擬 API 呼叫?

要開始使用 Jest,你只需要安裝它:npm install jest –save-dev

並在你的 package.json 檔案中包含一個測試命令,如下所示:

"scripts":{<br>
  "test":" jest"<br>
}

Jest 最初是 Jasmine 的一個分支,因此您可以執行我們上面描述的所有操作,甚至更多。“describe” 塊的基本模式和包含一個或多個 “expect” 方法的 “it” 塊在 Jest 中的工作原理相同。

到目前為止,我們一直在測試確定性函式(對於給定的輸入,它們始終具有相同的輸出)。但如果我們的 API 依賴於我們無法控制的東西,該怎麼辦?例如,如果我們的 API 使用第三方登入進行身份驗證,該怎麼辦?

Jest 允許您建立模擬函式,該函式返回可預測的結果,幷包含額外的方法來跟蹤函式如何與 API 整合。使用 jest.fn 方法,我們可以做出如下斷言:

<strong>describe</strong>('AJAX functions with Jest', () =&gt; {<br>
  <strong>const</strong> mockUrl = '/api/users';<br>
  <strong>const</strong> mockUsers = [{ name: 'jack', name: 'jill' }];<br>
  <strong>const</strong> getUsers = jest.<strong>fn</strong>(url =&gt; mockUsers);<br>
  <strong>it</strong>('returns returns users from an api call', () =&gt; {<br>
    <strong>expect</strong>(<strong>getUsers</strong>(mockUrl)).<strong>toBe</strong>(mockUsers);<br>
    console.<strong>log</strong>(getUsers);<br>
  });<br>
  <strong>it</strong>('called getUser with a mockUrl', () =&gt; {<br>
    <strong>expect</strong>(getUsers).<strong>toHaveBeenCalledWith</strong>(mockUrl);<br>
  });<br>
});

如果你執行這個測試並檢視 console.log,你會注意到有很多方法與這個模擬函式相關聯。這些方法允許你具體定義函式的呼叫方式、函式應返回的內容等等。

您還可以使用 模擬整個模組(用 jest mock 函式替換它們的方法)。例如,您可以匯入 HTTP 庫(例如 Axios)並像這樣jest.mock()設定其方法的返回值:.get()

<strong>const</strong> axios = require('axios');<br>
jest.<strong>mock</strong>('axios');<br>
<br>
<strong>class</strong> <strong>Users</strong> {<br>
  <strong>static</strong> <strong>all</strong>() {<br>
    <strong>return</strong> axios.<strong>get</strong>('/users.json').<strong>then</strong>(resp =&gt; resp.data);<br>
  }<br>
}<br>
<strong>const</strong> mockUsers = [{ name: 'Jack' }];<br>
<strong>const</strong> mockResponse = { data: mockUsers };<br>
axios.get.<strong>mockResolvedValue</strong>(mockResponse);<br>
<br>
<strong>return</strong> <strong>Users</strong>.<strong>all</strong>().<strong>then</strong>(data =&gt; <strong>expect</strong>(data).<strong>toEqual</strong>(users));

Jasmine 還有一個用於模擬 AJAX 呼叫的外掛 ( jasmine-ajax ),但它不像 Jest 那樣靈活。它用自定義響應替換瀏覽器中的 XMLHttpRequest 物件。由於 XMLHttpRequest 存在於 DOM 中,因此您需要建立一個假 DOM(使用類似 jsdom 的東西)才能在後端執行它。

Jasmine、Jest 和其他替代方案有哪些?

Jasmine 和 Jest 有很多相似之處。如果您的 API 主要由純函式組成,那麼 Jest 和 Jasmine 都是不錯的選擇,可確保您的 API 按預期執行。

Jasmine 比 Jest 更快、更輕量,但功能較少。在控制檯中執行測試時,Jest 更具描述性,但如果您更注重簡約,您可能更喜歡 Jasmine。我們欣賞 Jest 中模擬函式的靈活性,因此對於複雜的 API,我們建議使用 Jest 而不是 Jasmine。

但是,使用 Jest 和 Jasmine 等測試框架也有幾個缺點。如果您有多個團隊負責應用程式的不同部分(例如:前端與後端,或原生與 Web),您可能需要為每個單獨的環境編寫和更新測試。此外,由於您實際上並未部署到網路,因此 API 測試環境中可能會遺漏一些細節。

出於這些原因,如果您正在模擬仍在開發中的 API,那麼在本地或雲端生成模擬伺服器會很有用。Stoplight 有一個開源模擬伺服器,它可以根據 OpenAPI 文件生成。立即開始並在獲得實時資料之前設定您的 API 測試,我們希望您發現這些 API 測試最佳實踐很有幫助!

相關文章