搜索
首页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
JavaScript:探索网络语言的多功能性JavaScript:探索网络语言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是现代Web开发的核心语言,因其多样性和灵活性而广泛应用。1)前端开发:通过DOM操作和现代框架(如React、Vue.js、Angular)构建动态网页和单页面应用。2)服务器端开发:Node.js利用非阻塞I/O模型处理高并发和实时应用。3)移动和桌面应用开发:通过ReactNative和Electron实现跨平台开发,提高开发效率。

JavaScript的演变:当前的趋势和未来前景JavaScript的演变:当前的趋势和未来前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

神秘的JavaScript:它的作用以及为什么重要神秘的JavaScript:它的作用以及为什么重要Apr 09, 2025 am 12:07 AM

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python还是JavaScript更好?Python还是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。 1.Python以简洁语法和丰富库生态着称,适用于数据分析和Web开发。 2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

如何安装JavaScript?如何安装JavaScript?Apr 05, 2025 am 12:16 AM

JavaScript不需要安装,因为它已内置于现代浏览器中。你只需文本编辑器和浏览器即可开始使用。1)在浏览器环境中,通过标签嵌入HTML文件中运行。2)在Node.js环境中,下载并安装Node.js后,通过命令行运行JavaScript文件。

在Quartz中如何在任务开始前发送通知?在Quartz中如何在任务开始前发送通知?Apr 04, 2025 pm 09:24 PM

如何在Quartz中提前发送任务通知在使用Quartz定时器进行任务调度时,任务的执行时间是由cron表达式设定的。现�...

在JavaScript中,如何在构造函数中获取原型链上函数的参数?在JavaScript中,如何在构造函数中获取原型链上函数的参数?Apr 04, 2025 pm 09:21 PM

在JavaScript中如何获取原型链上函数的参数在JavaScript编程中,理解和操作原型链上的函数参数是常见且重要的任�...

微信小程序webview中Vue.js动态style位移失效是什么原因?微信小程序webview中Vue.js动态style位移失效是什么原因?Apr 04, 2025 pm 09:18 PM

在微信小程序web-view中使用Vue.js动态style位移失效的原因分析在使用Vue.js...

See all articles

热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中的所有内容
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

功能强大的PHP集成开发环境

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

Atom编辑器mac版下载

Atom编辑器mac版下载

最流行的的开源编辑器