您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

实用的测试驱动开发方法

2024/3/4 9:05:18发布24次查看
什么是测试驱动开发?测试驱动开发(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 是程序员工具带中的一项有用技能。随着时间的推移,您会对何时应用此方法产生直觉。
感谢您的阅读,请在评论部分告诉我们您的想法。
以上就是实用的测试驱动开发方法的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product