什么是测试驱动开发?
测试驱动开发(TDD)只是意味着您首先编写测试。 您甚至在编写单行业务逻辑之前就对正确的代码设定了期望。 TDD不仅有助于确保您的代码正确无误,而且还可以帮助您编写较小的函数,在不破坏功能的情况下重构代码并更好地理解问题。
在本文中,我将通过构建一个小型实用程序来介绍TDD的一些概念。 我们还将介绍TDD使您的生活变得简单的一些实际情况。
使用TDD构建HTTP客户端
我们将要建设的
我们将逐步构建一个抽象HTTP谓词的简单HTTP客户端。 为了使重构顺利进行,我们将遵循TDD做法。 我们将使用Jasmine,Sinn和Karma进行测试。 首先, 请从示例项目中复制package.json , karma.conf.js和webpack.test.js ,或从GitHub repo克隆示例项目 。
如果您了解新的Fetch API的工作原理会有所帮助,但是示例应易于遵循。 对于初学者来说, Fetch API是XMLHttpRequest的更好替代方案。 它简化了网络交互,并与Promises一起很好地工作。
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声明并监视对象。 (茉莉花有自己的间谍方式和测试存根,但我更喜欢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);
};
如果现在运行测试,将会看到一个失败的响应,提示Expected [object Response] to equal 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令牌。
为此,首先将一个options对象作为第三个参数传递给我们的方法。
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)));
};
看那个! 即使重构了核心组件,我们的测试仍然通过。
处理补丁请求
另一个常用的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是程序员工具带中的一项有用技能。 随着时间的流逝,您将逐渐了解何时应用此功能。
感谢您的阅读,请在评论部分告诉我们您的想法。
翻译自: https://code.tutsplus.com/tutorials/practical test driven development--cms-30345