Table of Contents
What is test-driven development?
Building an HTTP client using TDD
What we will build
GET PACKAGING
Add query parameters
Handling mutations
替代内容类型和 CSRF 令牌
编码形式
处理 PATCH 请求
何时进行 TDD?
总结
Home Web Front-end JS Tutorial Practical test-driven development methods

Practical test-driven development methods

Sep 03, 2023 pm 05:05 PM
automated test continuous integration test driven development (tdd)

Practical test-driven development methods

What is test-driven development?

Test Driven Development (TDD) simply means that you write the tests first. Before writing a line of business logic, you can pre-set expectations for correct code. TDD not only helps ensure that your code is correct, but it also helps you write smaller functions, refactor your code without breaking functionality, and better understand your problem.

In this article, I will introduce some concepts of TDD by building a small utility program. We'll also cover some practical scenarios where TDD will make your life easier.

Building an HTTP client using TDD

What we will build

We will gradually build a simple HTTP client that abstracts various HTTP verbs. To make refactoring go smoothly, we will follow TDD practices. We will use Jasmine, Sinon and Karma for testing. First, copy package.json, karma.conf.js, and webpack.test.js from the sample project, or clone the sample project directly from the GitHub repository.

It will help if you understand how the new Fetch API works, but these examples should be easy to understand. For newbies, Fetch API is a better alternative to XMLHttpRequest. It simplifies network interaction and works well with Promises.

GET PACKAGING

First, create an empty file at src/http.js and an accompanying test file at src/__tests__/http-test.js.

Let's set up a test environment for this service.

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

We are using Jasmine and Sinon here - Jasmine defines test scenarios, Sinon assertions and monitoring objects. (Jasmine has its own way of monitoring and stubbing tests, but I prefer Sinon's API.)

The code above is self-explanatory. Before each test run, we hijack the call to the Fetch API since there is no server available and return a mock Promise object. The goal here is to unit test whether the Fetch API is called with the correct parameters and see if the wrapper handles any network errors correctly.

Let’s start with the failing test case:

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

Start the test runner by calling karma start. Now the test will obviously fail because there is no get method in http. Let's correct this problem.

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);
};
Copy after login

If you run the test now, you will see the failed response, showing Expected [object Response] equal to Object({ }). The response is a Stream object. As the name suggests, a stream object is a data stream. To get data from a stream, you need to first read the stream using some of its helper methods. Now, we can assume the stream is JSON and deserialize it by calling 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)));
};
Copy after login

Our test suite should now be green.

Add query parameters

So far, the get method just makes a simple call without any query parameters. Let's write a failing test and see how it handles query parameters. If we pass { users: [1, 2], limit: 50, isDetailed: false } as query parameters, our HTTP client should make a network call to /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();
      })
  });
Copy after login

Now that we have the test set up, let's extend the get method to handle the query parameters.

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)));
};
Copy after login

If the parameter is present, we construct a query string and append it to the URL.

Here, I'm using the Query String Library - a nice little helper library that helps with various query parameter scenarios.

Handling mutations

GET is probably the simplest HTTP method to implement. GET is idempotent and should not be used for any mutations. POST usually means updating some records in the server. This means that POST requests require some safeguards by default, such as CSRF tokens. More on this in the next section.

Let’s first build a test for a basic POST request:

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

The signature of POST is very similar to that of GET. It requires an options attribute where you define the header, body, and most importantly the method. The method describes the HTTP verb, in this case "post".

Now, let's assume the content type is JSON and start implementing the POST request.

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),
  });
};
Copy after login

At this point, our post method is very primitive. It doesn't support anything other than JSON requests.

替代内容类型和 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();
        });
    });
Copy after login

当我们提供 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'
}
Copy after login

请注意,获取 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();
        });
    });
Copy after login

让我们提取一个小辅助方法来完成这项繁重的工作。基于 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)));
};
Copy after login

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

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

});
});
Copy after login

类似地,我们可以通过使动词可配置来重用当前的 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');

Copy after login

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

export const patch = (url, params, options = {}) => request(url, params, options, 'patch');
Copy after login

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

何时进行 TDD?

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

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

总结

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

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

The above is the detailed content of Practical test-driven development methods. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

AI Hentai Generator

AI Hentai Generator

Generate AI Hentai for free.

Hot Article

R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Best Graphic Settings
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. How to Fix Audio if You Can't Hear Anyone
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25: How To Unlock Everything In MyRise
4 weeks ago By 尊渡假赌尊渡假赌尊渡假赌

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

How to use Python scripts to implement automated testing in Linux environment How to use Python scripts to implement automated testing in Linux environment Oct 05, 2023 am 11:51 AM

How to use Python scripts to implement automated testing in the Linux environment. With the rapid development of software development, automated testing plays a vital role in ensuring software quality and improving development efficiency. As a simple and easy-to-use programming language, Python has strong portability and development efficiency, and is widely used in automated testing. This article will introduce how to use Python to write automated test scripts in a Linux environment and provide specific code examples. Environment Preparation for Automation in Linux Environment

Jenkins in PHP Continuous Integration: Master of Build and Deployment Automation Jenkins in PHP Continuous Integration: Master of Build and Deployment Automation Feb 19, 2024 pm 06:51 PM

In modern software development, continuous integration (CI) has become an important practice to improve code quality and development efficiency. Among them, Jenkins is a mature and powerful open source CI tool, especially suitable for PHP applications. The following content will delve into how to use Jenkins to implement PHP continuous integration, and provide specific sample code and detailed steps. Jenkins installation and configuration First, Jenkins needs to be installed on the server. Just download and install the latest version from its official website. After the installation is complete, some basic configuration is required, including setting up an administrator account, plug-in installation, and job configuration. Create a new job On the Jenkins dashboard, click the "New Job" button. Select "Frees

What are the best practices for PHP packaging and deployment? What are the best practices for PHP packaging and deployment? Jul 30, 2023 am 11:25 AM

What are the best practices for PHP packaging and deployment? With the rapid development of Internet technology, PHP, as an open source programming language widely used in website development, more and more developers need to improve efficiency and stability in project deployment. This article will introduce several best practices for PHP packaging and deployment and provide relevant code examples. Use version control tools Version control tools such as Git, SVN, etc. can help developers effectively manage code changes. Use version control tools to easily track and roll back code, ensuring every deployment is

C# Development Advice: Continuous Integration and Continuous Delivery Practices C# Development Advice: Continuous Integration and Continuous Delivery Practices Nov 22, 2023 pm 05:28 PM

In the current software development process, continuous integration (ContinuousIntegration) and continuous delivery (ContinuousDelivery) have become key practices for development teams to improve product quality and speed up delivery. Whether you're a large software enterprise or a small team, you can benefit from both areas. This article will provide C# developers with some suggestions on continuous integration and continuous delivery practices. Automated builds and tests Automated builds and tests are the foundation of continuous integration. make

Use Webman to implement continuous integration and deployment of websites Use Webman to implement continuous integration and deployment of websites Aug 25, 2023 pm 01:48 PM

Using Webman to achieve continuous integration and deployment of websites With the rapid development of the Internet, the work of website development and maintenance has become more and more complex. In order to improve development efficiency and ensure website quality, continuous integration and deployment have become an important choice. In this article, I will introduce how to use the Webman tool to implement continuous integration and deployment of the website, and attach some code examples. 1. What is Webman? Webman is a Java-based open source continuous integration and deployment tool that provides

PHP Jenkins and SonarQube: Continuously monitor PHP code quality PHP Jenkins and SonarQube: Continuously monitor PHP code quality Mar 09, 2024 pm 01:10 PM

In PHP development, maintaining code quality is crucial to improve software reliability, maintainability, and security. Continuously monitoring code quality proactively identifies issues, promotes early fixes, and prevents them from reaching production. In this article, we will explore how to set up a continuous monitoring pipeline for a PHP project using Jenkins and SonarQube. Jenkins: Continuous Integration Server Jenkins is an open source continuous integration server that automates the build, test and deployment process. It allows developers to set up jobs that will be triggered periodically and perform a series of tasks. For PHP projects, we can set up Jenkins jobs to complete the following tasks: check out the code from the version control system

How to use React and Jenkins to build front-end applications for continuous integration and continuous deployment How to use React and Jenkins to build front-end applications for continuous integration and continuous deployment Sep 27, 2023 am 08:37 AM

How to use React and Jenkins to build front-end applications with continuous integration and continuous deployment Introduction: In today's Internet development, continuous integration and continuous deployment have become important means for development teams to improve efficiency and ensure product quality. As a popular front-end framework, React, combined with Jenkins, a powerful continuous integration tool, can provide us with a convenient and efficient solution for building front-end applications for continuous integration and continuous deployment. This article will introduce in detail how to use React and Jenkins to support

How to perform continuous integration code coverage analysis in GitLab How to perform continuous integration code coverage analysis in GitLab Oct 20, 2023 pm 04:27 PM

Title: Code coverage analysis and examples in GitLab continuous integration Introduction: As software development becomes more and more complex, code coverage analysis has become one of the important indicators to evaluate the quality of software testing. Using continuous integration to conduct code coverage analysis can help development teams monitor their code quality in real time and improve software development efficiency. This article will introduce how to perform continuous integration code coverage analysis in GitLab and provide specific code examples. 1. Code coverage analysis in GitLab 1.1 Code coverage

See all articles