目錄
什麼是測試驅動開發?
使用 TDD 建構 HTTP 用戶端
我們將建構什麼
GET 的包裝
新增查詢參數
處理突變
替代内容类型和 CSRF 令牌
编码形式
处理 PATCH 请求
何时进行 TDD?
总结
首頁 web前端 js教程 實用的測試驅動開發方法

實用的測試驅動開發方法

Sep 03, 2023 pm 05:05 PM
自動化測試 持續集成 測試驅動開發(tdd)

實用的測試驅動開發方法

什麼是測試驅動開發?

測試驅動開發(TDD)僅表示您先編寫測試。在編寫一行業務邏輯之前,您可以預先設定正確程式碼的期望。 TDD 不僅有助於確保您的程式碼正確,還可以幫助您編寫更小的函數,在不破壞功能的情況下重構程式碼,並更好地理解您的問題。

在本文中,我將透過建立一個小型實用程式來介紹 TDD 的一些概念。我們還將介紹一些 TDD 將使您的生活變得簡單的實際場景。

使用 TDD 建構 HTTP 用戶端

我們將建構什麼

我們將逐步建立一個簡單的 HTTP 用戶端,用於抽象化各種 HTTP 動詞。為了使重構順利進行,我們將遵循TDD實踐。我們將使用 Jasmine、Sinon 和 Karma 進行測試。首先,從範例專案複製 package.json、karma.conf.js 和 webpack.test.js,或直接從 GitHub 儲存庫複製範例專案。

如果您了解新的 Fetch API 的工作原理,將會有所幫助,但這些範例應該很容易理解。對於新手來說,Fetch API 是 XMLHttpRequest 的更好替代方案。它簡化了網路互動並與 Promise 配合良好。

GET 的包裝

首先,在 src/http.js 處建立一個空文件,並在 src/__tests__/http-test.js 下建立一個隨附的測試文件。

讓我們為此服務設定一個測試環境。

import * as http from "../http.js";
import sinon from "sinon";
import * as fetch from "isomorphic-fetch";

describe("TestHttpService", () => {
  describe("Test success scenarios", () => {
    beforeEach(() => {
      stubedFetch = sinon.stub(window, "fetch");

      window.fetch.returns(Promise.resolve(mockApiResponse()));

      function mockApiResponse(body = {}) {
        return new window.Response(JSON.stringify(body), {
          status: 200,
          headers: { "Content-type": "application/json" }
        });
      }
    });
  });
});
登入後複製

我們在這裡使用 Jasmine 和 Sinon — Jasmine 定義測試場景,Sinon 斷言和監視物件。 (Jasmine 有自己的方式來監視和存根測試,但我更喜歡 Sinon 的 API。)

上面的程式碼是不言自明的。在每次測試運行之前,我們都會劫持對 Fetch API 的調用,因為沒有可用的伺服器,並返回一個模擬 Promise 物件。這裡的目標是對 Fetch API 是否使用正確的參數呼叫進行單元測試,並查看包裝器是否能夠正確處理任何網路錯誤。

讓我們從失敗的測試案例開始:

 describe("Test get requests", () => {
  it("should make a GET request", done => {
    http.get(url).then(response => {
      expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
      expect(response).toEqual({});
      done();
    });
  });
});
登入後複製

透過呼叫 karma start 啟動測試運行程序。現在測試顯然會失敗,因為 http 中沒有 get 方法。讓我們來修正這個問題。

const status = response => {
  if (response.ok) {
    return Promise.resolve(response);
  }

  return Promise.reject(new Error(response.statusText));
};

export const get = (url, params = {}) => {
  return fetch(url)
    .then(status);
};
登入後複製

如果您現在執行測試,您將看到失敗的回應,顯示 預期 [object Response] 等於 Object({  })。響應是一個 Stream 物件。顧名思義,流物件都是一個資料流。要從流中獲取數據,您需要先使用流的一些輔助方法來讀取流。現在,我們可以假設流是 JSON 並透過呼叫 response.json() 對其進行反序列化。

const deserialize = response => response.json();

export const get = (url, params = {}) => {
  return fetch(url)
    .then(status)
    .then(deserialize)
    .catch(error => Promise.reject(new Error(error)));
};
登入後複製

我們的測試套件現在應該是綠色的。

新增查詢參數

到目前為止,get 方法只是進行了一個簡單的調用,沒有任何查詢參數。讓我們來寫一個失敗的測試,看看它如何處理查詢參數。如果我們傳遞{ users: [1, 2], limit: 50, isDetailed: false } 作為查詢參數,我們的HTTP 用戶端應該對/api 進行網路呼叫/v1/users/ ?users=1&users=2&limit=50&isDetailed=false.

  it("should serialize array parameter", done => {
    const users = [1, 2];
    const limit = 50;
    const isDetailed = false;
    const params = { users, limit, isDetailed };
    http
      .get(url, params)
      .then(response => {
        expect(stubedFetch.calledWith(`${url}?isDetailed=false&limit=50&users=1&users=2/`)).toBeTruthy();
        done();
      })
  });
登入後複製

現在我們已經設定了測試,讓我們擴展 get 方法來處理查詢參數。

import { stringify } from "query-string";

export const get = (url, params) => {
  const prefix = url.endsWith('/') ? url : `${url}/`;
  const queryString = params ? `?${stringify(params)}/` : '';
  
  return fetch(`${prefix}${queryString}`)
    .then(status)
    .then(deserializeResponse)
    .catch(error => Promise.reject(new Error(error)));
};
登入後複製

如果參數存在,我們將建構一個查詢字串並將其附加到 URL 中。

在這裡,我使用了查詢字串庫 - 這是一個很好的小幫助程式庫,有助於處理各種查詢參數場景。

處理突變

GET 可能是實作最簡單的 HTTP 方法。 GET 是冪等的,不應該用於任何突變。 POST 通常意味著更新伺服器中的一些記錄。這意味著 POST 請求預設需要一些防護措施,例如 CSRF 令牌。下一節將詳細介紹這一點。

讓我們先建立一個基本 POST 請求的測試:

describe(`Test post requests`, () => {

    it("should send request with custom headers", done => {
        const postParams = { 
        users: [1, 2] 
        };
        http.post(url, postParams, { contentType: http.HTTP_HEADER_TYPES.text })
        .then(response => {
            const [uri, params] = [...stubedFetch.getCall(0).args];

            expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
            expect(params.body).toEqual(JSON.stringify(postParams));

            expect(params.headers.get("Content-Type")).toEqual(http.HTTP_HEADER_TYPES.text);
            done();
        });
    });
});
登入後複製

POST 的簽章與 GET 非常相似。它需要一個 options 屬性,您可以在其中定義標頭、正文,以及最重要的 method。此方法描述了 HTTP 動詞,在本例中為 "post"

現在,我們假設內容類型是 JSON 並開始實作 POST 請求。

export const HTTP_HEADER_TYPES = {
  json: "application/json",
  text: "application/text",
  form: "application/x-www-form-urlencoded",
  multipart: "multipart/form-data"
};


export const post = (url, params) => {

  const headers = new Headers();

  headers.append("Content-Type", HTTP_HEADER_TYPES.json);

  return fetch(url, {
    headers,
    method: "post",
    body: JSON.stringify(params),
  });
};
登入後複製

此時,我們的post方法就非常原始了。除了 JSON 請求之外,它不支援任何其他內容。

替代内容类型和 CSRF 令牌

让我们允许调用者决定内容类型,并将 CSRF 令牌投入战斗。根据您的要求,您可以将 CSRF 设为可选。在我们的用例中,我们将假设这是一个选择加入功能,并让调用者确定是否需要在标头中设置 CSRF 令牌。

为此,首先将选项对象作为第三个参数传递给我们的方法。

   it("should send request with CSRF", done => {
        const postParams = { 
        users: [1, 2 ] 
        };
        http.post(url, postParams, {
            contentType: http.HTTP_HEADER_TYPES.text,
            includeCsrf: true 
        }).then(response => {
            const [uri, params] = [...stubedFetch.getCall(0).args];

            expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
            expect(params.body).toEqual(JSON.stringify(postParams));
            expect(params.headers.get("Content-Type")).toEqual(http.HTTP_HEADER_TYPES.text);
            expect(params.headers.get("X-CSRF-Token")).toEqual(csrf);

            done();
        });
    });
登入後複製

当我们提供 options{contentType: http.HTTP_HEADER_TYPES.text,includeCsrf: true 时,它应该相应地设置内容标头和 CSRF 标头。让我们更新 post 函数以支持这些新选项。

export const post = (url, params, options={}) => {
  const {contentType, includeCsrf} = options;

  const headers = new Headers();

  headers.append("Content-Type", contentType || HTTP_HEADER_TYPES.json());
  if (includeCsrf) {
    headers.append("X-CSRF-Token", getCSRFToken());
  }

  return fetch(url, {
    headers,
    method: "post",
    body: JSON.stringify(params),
  });
};

const getCsrfToken = () => {
    //This depends on your implementation detail
    //Usually this is part of your session cookie
    return 'csrf'
}
登入後複製

请注意,获取 CSRF 令牌是一个实现细节。通常,它是会话 cookie 的一部分,您可以从那里提取它。我不会在本文中进一步讨论它。

您的测试套件现在应该很满意。

编码形式

我们的 post 方法现在已经成型,但是在发送正文时仍然很简单。您必须针对每种内容类型以不同的方式处理数据。处理表单时,我们应该在通过网络发送数据之前将数据编码为字符串。

   it("should send a form-encoded request", done => {
        const users = [1, 2];
        const limit = 50;
        const isDetailed = false;
        const postParams = { users, limit, isDetailed };

        http.post(url, postParams, {
            contentType: http.HTTP_HEADER_TYPES.form,
            includeCsrf: true 
        }).then(response => {
            const [uri, params] = [...stubedFetch.getCall(0).args];

            expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
            expect(params.body).toEqual("isDetailed=false&limit=50&users=1&users=2");
            expect(params.headers.get("Content-Type")).toEqual(http.HTTP_HEADER_TYPES.form);
            expect(params.headers.get("X-CSRF-Token")).toEqual(csrf);

            done();
        });
    });
登入後複製

让我们提取一个小辅助方法来完成这项繁重的工作。基于 contentType,它对数据的处理方式有所不同。

const encodeRequests = (params, contentType) => {

  switch (contentType) {
    case HTTP_HEADER_TYPES.form: {
      return stringify(params);
    }
    
    default:
      return JSON.stringify(params);
  }
}

export const post = (url, params, options={}) => {
  const {includeCsrf, contentType} = options;

  const headers = new Headers();

  headers.append("Content-Type", contentType || HTTP_HEADER_TYPES.json);


  if (includeCsrf) {
    headers.append("X-CSRF-Token", getCSRFToken());
  }

  return fetch(url, {
    headers,
    method="post",
    body: encodeRequests(params, contentType || HTTP_HEADER_TYPES.json)
  }).then(deserializeResponse)
  .catch(error => Promise.reject(new Error(error)));
};
登入後複製

看看那个!即使在重构核心组件之后,我们的测试仍然可以通过。

处理 PATCH 请求

另一个常用的 HTTP 动词是 PATCH。现在,PATCH 是一个变异调用,这意味着这两个操作的签名非常相似。唯一的区别在于 HTTP 动词。通过简单的调整,我们可以重用为 POST 编写的所有测试。

['post', 'patch'].map(verb => {

describe(`Test ${verb} requests`, () => {
let stubCSRF, csrf;

beforeEach(() => {
  csrf = "CSRF";
  stub(http, "getCSRFToken").returns(csrf);
});

afterEach(() => {
  http.getCSRFToken.restore();
});

it("should send request with custom headers", done => {
  const postParams = { 
    users: [1, 2] 
  };
  http[verb](url, postParams, { contentType: http.HTTP_HEADER_TYPES.text })
    .then(response => {
      const [uri, params] = [...stubedFetch.getCall(0).args];

      expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
      expect(params.body).toEqual(JSON.stringify(postParams));

      expect(params.headers.get("Content-Type")).toEqual(http.HTTP_HEADER_TYPES.text);
      done();
    });
});

it("should send request with CSRF", done => {
  const postParams = { 
    users: [1, 2 ] 
  };
  http[verb](url, postParams, {
      contentType: http.HTTP_HEADER_TYPES.text,
      includeCsrf: true 
    }).then(response => {
      const [uri, params] = [...stubedFetch.getCall(0).args];

      expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
      expect(params.body).toEqual(JSON.stringify(postParams));
      expect(params.headers.get("Content-Type")).toEqual(http.HTTP_HEADER_TYPES.text);
      expect(params.headers.get("X-CSRF-Token")).toEqual(csrf);

      done();
    });
});

it("should send a form-encoded request", done => {
  const users = [1, 2];
  const limit = 50;
  const isDetailed = false;
  const postParams = { users, limit, isDetailed };

  http[verb](url, postParams, {
      contentType: http.HTTP_HEADER_TYPES.form,
      includeCsrf: true 
    }).then(response => {
      const [uri, params] = [...stubedFetch.getCall(0).args];

      expect(stubedFetch.calledWith(`${url}`)).toBeTruthy();
      expect(params.body).toEqual("isDetailed=false&limit=50&users=1&users=2");
      expect(params.headers.get("Content-Type")).toEqual(http.HTTP_HEADER_TYPES.form);
      expect(params.headers.get("X-CSRF-Token")).toEqual(csrf);

      done();
    });
});

});
});
登入後複製

类似地,我们可以通过使动词可配置来重用当前的 post 方法,并重命名方法名称以反映通用的内容。

const request = (url, params, options={}, method="post") => {
  const {includeCsrf, contentType} = options;

  const headers = new Headers();

  headers.append("Content-Type", contentType || HTTP_HEADER_TYPES.json);


  if (includeCsrf) {
    headers.append("X-CSRF-Token", getCSRFToken());
  }

  return fetch(url, {
    headers,
    method,
    body: encodeRequests(params, contentType)
  }).then(deserializeResponse)
  .catch(error => Promise.reject(new Error(error)));
};


export const post = (url, params, options = {}) => request(url, params, options, 'post');

登入後複製

现在我们所有的 POST 测试都已通过,剩下的就是为 patch 添加另一个方法。

export const patch = (url, params, options = {}) => request(url, params, options, 'patch');
登入後複製

很简单,对吧?作为练习,尝试自行添加 PUT 或 DELETE 请求。如果您遇到困难,请随时参考该存储库。

何时进行 TDD?

社区对此存在分歧。有些程序员一听到 TDD 这个词就逃跑并躲起来,而另一些程序员则靠它生存。只需拥有一个好的测试套件,您就可以实现 TDD 的一些有益效果。这里没有正确的答案。这完全取决于您和您的团队对您的方法是否满意。

根据经验,我使用 TDD 来解决需要更清晰的复杂、非结构化问题。在评估一种方法或比较多种方法时,我发现预先定义问题陈述和边界很有帮助。它有助于明确您的功能需要处理的需求和边缘情况。如果案例数量太多,则表明您的程序可能做了太多事情,也许是时候将其拆分为更小的单元了。如果需求很简单,我会跳过 TDD,稍后添加测试。

总结

关于这个话题有很多噪音,而且很容易迷失方向。如果我能给你一些临别建议的话:不要太担心 TDD 本身,而要关注基本原则。这一切都是为了编写干净、易于理解、可维护的代码。 TDD 是程序员工具带中的一项有用技能。随着时间的推移,您会对何时应用此方法产生直觉。

感谢您的阅读,请在评论部分告诉我们您的想法。

以上是實用的測試驅動開發方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Linux環境中用Python腳本實現自動化測試的方法 Linux環境中用Python腳本實現自動化測試的方法 Oct 05, 2023 am 11:51 AM

Linux環境中以Python腳本實現自動化測試的方法隨著軟體開發的快速發展,自動化測試在確保軟體品質和提高開發效率方面起著至關重要的作用。而Python作為一種簡單易用的程式語言,具有很強的可移植性和開發效率,被廣泛應用於自動化測試中。本文將介紹在Linux環境下使用Python編寫自動化測試腳本的方法,並提供具體程式碼範例。環境準備在Linux環境中進行自

PHP 持續整合中的 Jenkins:建置和部署自動化大師 PHP 持續整合中的 Jenkins:建置和部署自動化大師 Feb 19, 2024 pm 06:51 PM

在現代軟體開發中,持續整合(CI)已成為提高程式碼品質和開發效率的重要實踐。其中,jenkins是一個成熟且功能強大的開源CI工具,特別適用於PHP應用程式。以下內容將深入探討如何使用Jenkins實現php持續集成,並提供具體的範例程式碼和詳細的步驟。 Jenkins安裝和設定首先,需要在伺服器上安裝Jenkins。透過其官網下載並安裝最新版本即可。安裝完成後,需要進行一些基本配置,包括設定管理員帳戶、外掛程式安裝和作業配置。建立一個新作業在Jenkins儀表板上,點選"新作業"按鈕。選擇"Frees

PHP打包部署的最佳實務有哪些? PHP打包部署的最佳實務有哪些? Jul 30, 2023 am 11:25 AM

PHP打包部署的最佳實務有哪些?隨著網路技術的快速發展,PHP作為一種廣泛應用於網站開發的開源程式語言,越來越多的開發者需求在專案部署上提高效率和穩定性。本文將介紹幾種PHP打包部署的最佳實踐,並提供相關的程式碼範例。使用版本控制工具版本控制工具如Git、SVN等,可以幫助開發者有效管理程式碼的變更。使用版本控制工具可以輕鬆追蹤和回滾程式碼,確保每次部署都是

C#開發建議:持續整合與持續交付實踐 C#開發建議:持續整合與持續交付實踐 Nov 22, 2023 pm 05:28 PM

在目前的軟體開發過程中,持續整合(ContinuousIntegration)和持續交付(ContinuousDelivery)已經成為了開發團隊提高產品品質和加快交付速度的關鍵實踐。無論是大型軟體企業還是小型團隊,都可以從這兩個領域中受益。本文將為C#開發人員提供一些關於持續整合與持續交付實務的建議。自動化建置和測試自動化建置和測試是持續整合的基礎。使

使用Webman實現網站的持續整合與部署 使用Webman實現網站的持續整合與部署 Aug 25, 2023 pm 01:48 PM

使用Webman實現網站的持續整合和部署隨著網路的快速發展,網站開發和維護的工作也變得越來越複雜。為了提高開發效率和保證網站的質量,採用持續整合和部署的方式成為了一個重要的選擇。在這篇文章中,我將介紹如何使用Webman工具來實現網站的持續整合和部署,並附上一些程式碼範例。一、什麼是WebmanWebman是一個基於Java的開源持續整合和部署工具,它提供了

如何利用React和Jenkins來建構持續整合和持續部署的前端應用 如何利用React和Jenkins來建構持續整合和持續部署的前端應用 Sep 27, 2023 am 08:37 AM

如何利用React和Jenkins建構持續整合和持續部署的前端應用引言:在當今的互聯網開發中,持續整合和持續部署已經成為了開發團隊提昇效率、保障產品品質的重要手段。而React作為流行的前端框架,結合Jenkins這強大的持續整合工具,能夠為我們建構持續整合和持續部署的前端應用提供便利和高效的解決方案。本文將詳細介紹如何利用React和Jenkins進行持

如何在GitLab中進行持續整合的程式碼覆蓋率分析 如何在GitLab中進行持續整合的程式碼覆蓋率分析 Oct 20, 2023 pm 04:27 PM

標題:GitLab持續整合中的程式碼覆蓋率分析及實例引言:隨著軟體開發變得越來越複雜,程式碼覆蓋率分析成為了評估軟體測試品質的重要指標之一。而採用持續整合來進行程式碼覆蓋率分析可以幫助開發團隊即時監控自己的程式碼質量,提高軟體開發效率。本文將介紹如何在GitLab中進行持續整合的程式碼覆蓋率分析,並提供具體的程式碼範例。一、GitLab中的程式碼覆蓋率分析1.1程式碼覆蓋

PHP Jenkins 與 SonarQube:持續監控 PHP 程式碼品質 PHP Jenkins 與 SonarQube:持續監控 PHP 程式碼品質 Mar 09, 2024 pm 01:10 PM

在PHP開發中,維持程式碼品質至關重要,可提高軟體的可靠性、可維護性和安全性。持續監控程式碼品質可以主動發現問題,促進及早修復,並防止它們進入生產環境。在這篇文章中,我們將探討如何使用jenkins和SonarQube建立一個php專案的持續監控管道。 Jenkins:持續整合伺服器Jenkins是一個開源的持續整合伺服器,可自動化建置、測試和部署流程。它允許開發人員設定作業,這些作業將定期觸發並執行一系列任務。對於PHP項目,我們可以設定Jenkins作業來完成以下任務:從版本控制系統中檢出程式碼運

See all articles