>  기사  >  웹 프론트엔드  >  실용적인 테스트 중심 개발 방법

실용적인 테스트 중심 개발 방법

WBOY
WBOY원래의
2023-09-03 17:05:10500검색

실용적인 테스트 중심 개발 방법

테스트 중심 개발이란 무엇인가요?

테스트 중심 개발(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와 잘 작동합니다.

포장 받기

먼저 src/http.js에 빈 파일을 만들고 src/__tests__/http-test.js에 해당 테스트 파일을 만듭니다.

이 서비스에 대한 테스트 환경을 설정해 보겠습니다.

으아아아

여기에서는 Jasmine과 Sinon을 사용합니다. Jasmine은 테스트 시나리오 정의에, Sinon은 어설션 및 개체 모니터링에 사용됩니다. (Jasmine에는 자체적인 모니터링 및 스터빙 테스트 방식이 있지만 저는 Sinon의 API를 선호합니다.)

위의 코드는 설명이 필요하지 않습니다. 각 테스트를 실행하기 전에 사용 가능한 서버가 없기 때문에 Fetch API에 대한 호출을 하이재킹하고 모의 Promise 객체를 반환합니다. 여기서 목표는 Fetch API가 올바른 매개변수로 호출되는지 여부를 단위 테스트하고 래퍼가 네트워크 오류를 올바르게 처리하는지 확인하는 것입니다.

실패한 테스트 사례부터 시작해 보겠습니다.

으아아아

karma start 启动测试运行程序。现在测试显然会失败,因为 http 中没有 get 메소드를 호출합니다. 이 문제를 해결해 보겠습니다.

으아아아

지금 테스트를 실행하면 预期 [object Response] 等于 Object({  })。响应是一个 Stream 对象。顾名思义,流对象都是一个数据流。要从流中获取数据,您需要首先使用流的一些辅助方法来读取流。现在,我们可以假设流是 JSON 并通过调用 response.json() deserializing이라는 실패한 응답이 표시됩니다.

으아아아

이제 테스트 스위트는 녹색이 될 것입니다.

쿼리 매개변수 추가

지금까지 get 方法只是进行了一个简单的调用,没有任何查询参数。让我们编写一个失败的测试,看看它如何处理查询参数。如果我们传递 { users: [1, 2], limit: 50, isDetailed: false } 作为查询参数,我们的 HTTP 客户端应该对 /api 进行网络调用/v1/users/?users=1&users=2&limit=50&isDetailed=false. 으아아아

이제 테스트 설정이 완료되었으므로

메서드를 확장하여 쿼리 매개변수를 처리해 보겠습니다. get 으아아아

매개변수가 있으면 쿼리 문자열을 구성하여 URL에 추가합니다.

여기서는 쿼리 문자열 라이브러리를 사용했습니다. 이는 다양한 쿼리 매개변수 시나리오에 도움이 되는 작고 멋진 도우미 라이브러리입니다.

돌연변이 처리

GET은 아마도 구현하기 가장 간단한 HTTP 메소드일 것입니다. GET은 멱등원이므로 어떤 변형에도 사용하면 안 됩니다. POST는 일반적으로 서버의 일부 레코드를 업데이트하는 것을 의미합니다. 즉, POST 요청에는 기본적으로 CSRF 토큰과 같은 일부 보호 장치가 필요합니다. 이에 대한 자세한 내용은 다음 섹션에서 확인하세요.

기본 POST 요청에 대한 테스트를 구축하는 것부터 시작해 보겠습니다.

으아아아

POST의 서명은 GET과 매우 유사합니다.

가 필요합니다. options 属性,您可以在其中定义标头、正文,以及最重要的 method。该方法描述了 HTTP 动词,在本例中为 "post"

이제 콘텐츠 유형이 JSON이라고 가정하고 POST 요청 구현을 시작해 보겠습니다.

으아아아

이 시점에서 우리의

방법은 매우 원시적입니다. JSON 요청 이외의 다른 것은 지원하지 않습니다. post

替代内容类型和 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으로 문의하세요.