Rumah  >  Artikel  >  hujung hadapan web  >  Kaedah pembangunan berasaskan ujian praktikal

Kaedah pembangunan berasaskan ujian praktikal

WBOY
WBOYasal
2023-09-03 17:05:10498semak imbas

Kaedah pembangunan berasaskan ujian praktikal

Apakah pembangunan yang dipacu ujian?

Test Driven Development (TDD) bermaksud anda menulis ujian dahulu. Sebelum menulis baris logik perniagaan, anda boleh pratetapkan jangkaan untuk kod yang betul. TDD bukan sahaja membantu memastikan kod anda betul, tetapi ia juga membantu anda menulis fungsi yang lebih kecil, memfaktorkan semula kod anda tanpa merosakkan fungsi dan memahami masalah anda dengan lebih baik.

Dalam artikel ini, saya akan memperkenalkan beberapa konsep TDD dengan membina program utiliti kecil. Kami juga akan merangkumi beberapa senario praktikal di mana TDD akan menjadikan hidup anda lebih mudah.

Membina klien HTTP menggunakan TDD

Apa yang akan kami bina

Kami akan membina klien HTTP ringkas secara beransur-ansur yang mengabstrak pelbagai kata kerja HTTP. Untuk membuat pemfaktoran semula berjalan lancar, kami akan mengikuti amalan TDD. Kami akan menggunakan Jasmine, Sinon dan Karma untuk ujian. Mula-mula, salin package.json, karma.conf.js dan webpack.test.js daripada projek sampel atau klon projek sampel terus daripada repositori GitHub.

Ini akan membantu jika anda memahami cara API Ambil baharu berfungsi, tetapi contoh ini sepatutnya mudah diikuti. Untuk pemula, Fetch API ialah alternatif yang lebih baik kepada XMLHttpRequest. Ia memudahkan interaksi rangkaian dan berfungsi dengan baik dengan Promises.

DAPATKAN pembungkusan

Pertama, buat fail kosong di src/http.js dan buat fail yang disertakan di bawah src/__tests__/http-test.js fail ujian.

Mari sediakan persekitaran ujian untuk perkhidmatan ini.

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" }
        });
      }
    });
  });
});

Kami menggunakan Jasmine dan Sinon di sini - Jasmine mentakrifkan senario ujian, pernyataan Sinon dan objek pemantauan. (Jasmine mempunyai cara tersendiri untuk memantau dan ujian stub, tetapi saya lebih suka API Sinon.)

Kod di atas adalah jelas. Sebelum setiap ujian dijalankan, kami merampas panggilan ke API Ambil kerana tiada pelayan tersedia dan mengembalikan objek janji palsu. Matlamat di sini adalah untuk menguji unit sama ada API Ambil dipanggil dengan parameter yang betul dan melihat sama ada pembalut mengendalikan sebarang ralat rangkaian dengan betul.

Mari kita mulakan dengan kes ujian yang gagal:

 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();
    });
  });
});

Mulakan pelari ujian dengan memanggil karma start. Kini ujian itu jelas akan gagal kerana tiada kaedah get dalam http. Mari kita betulkan masalah ini. 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

   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();
        });
    });

Jika anda menjalankan ujian sekarang, anda akan melihat respons yang gagal, menunjukkan Dijangka [Respons objek] sama dengan Object({ }). Responsnya ialah objek Stream. Seperti namanya, objek aliran ialah aliran data. Untuk mendapatkan data daripada strim, anda perlu terlebih dahulu membaca strim menggunakan beberapa kaedah pembantunya. Sekarang, kita boleh menganggap bahawa strim itu ialah JSON dan menyahsirinya dengan memanggil response.json(). #🎜🎜#
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'
}
#🎜🎜# Suite ujian kami kini sepatutnya berwarna hijau. #🎜🎜#

Tambah parameter pertanyaan

#🎜🎜#Setakat ini, kaedah get hanya membuat panggilan mudah tanpa sebarang parameter pertanyaan. Mari tulis ujian yang gagal dan lihat cara ia mengendalikan parameter pertanyaan. Jika kami menghantar { users: [1, 2], limit: 50, isDetailed: false } sebagai parameter pertanyaan, klien HTTP kami harus bertindak balas kepada /api membuat panggilan rangkaian/v1/users/?users=1&users=2&limit=50&isDetailed=false #🎜🎜#.
   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();
        });
    });
#🎜🎜#Sekarang kita telah menyediakan ujian, mari lanjutkan kaedah get untuk mengendalikan parameter pertanyaan. #🎜🎜#
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)));
};
#🎜🎜#Jika parameter ada, kami akan membina rentetan pertanyaan dan menambahkannya pada URL. #🎜🎜# #🎜🎜#Di sini, saya menggunakan Pustaka Rentetan Pertanyaan - perpustakaan pembantu kecil yang bagus yang membantu dengan pelbagai senario parameter pertanyaan. #🎜🎜# #🎜🎜#menghadapi mutasi#🎜🎜# #🎜🎜#GET mungkin kaedah HTTP yang paling mudah untuk dilaksanakan. GET adalah idempoten dan tidak boleh digunakan untuk sebarang mutasi. POST biasanya bermaksud mengemas kini beberapa rekod dalam pelayan. Ini bermakna permintaan POST memerlukan beberapa perlindungan secara lalai, seperti token CSRF. Lebih lanjut mengenai perkara ini dalam bahagian seterusnya. #🎜🎜# #🎜🎜# Mari mula-mula bina ujian untuk permintaan POST asas: #🎜🎜#
['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();
    });
});

});
});
#🎜🎜#Tandatangan POST sangat serupa dengan GET. Ia memerlukan atribut options di mana anda mentakrifkan pengepala, kandungan dan yang paling penting kaedah. Kaedah ini menerangkan kata kerja HTTP, dalam kes ini "post". #🎜🎜# #🎜🎜#Sekarang, mari kita anggap jenis kandungan ialah JSON dan mula melaksanakan permintaan 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');

#🎜🎜#Pada ketika ini, kaedah post kami adalah sangat primitif. Ia tidak menyokong apa-apa selain permintaan 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 是程序员工具带中的一项有用技能。随着时间的推移,您会对何时应用此方法产生直觉。

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

Atas ialah kandungan terperinci Kaedah pembangunan berasaskan ujian praktikal. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn