티스토리 뷰

"단위 테스트를 어디에 쓸까?"하고 정말로 그렇게 묻는다면, 정말로 정말로는 그거 없이도 프로그램은 돌아간다고 말을 할 수가 있을 것입니다. 테스트는 그런 자리에서 의미가 있는 것 같습니다, 어플리케이션의 목적에 기여하는 자리는 아니고 프로젝트 정도에 기여하는. 

 

몇 가지 리팩터링을 계획하고 있습니다. downscaleImage를 타입스크립트로 작성하여 매개변수를 어떻게 전달해야하는지, 반환값은 어떤 레이아웃인지 알기 쉽도록 할 예정입니다. 그리고 타입스크립트로 변환한 이 함수 내부의 조건 분기를 더 작은 함수로 나누어 논리 흐름을 간결하게 합니다. 이 작업들이 완료되면 트랜스파일러 없이도 함수가 사용 가능하도록 레거시 자바스크립트 코드로 다시 작성합니다. (바벨의 결과물은 휴먼리더블하진 않습니다.)

  1. 타입스크립트 변환

  2. 함수 분할

  3. 레거시 지원

매 작업 절차마다 산출된 프로그램(함수)은 원래의 downscaleImage와 입출력 결과가 같아야 합니다. 브라우저에 파일 타입 입력 개체를 만들고, 매 함수를 작성할 때마다 브라우저를 열고, 파일을 직접 선택하여 결과물을 확인하는 것보다 테스트 프로그램을 작성하여 이 작업을 자동화합니다. 브라우저 환경을 jsdom으로 가상화하는 Jest 대신, 직접 브라우저에서 테스트를 실행하는 Karma를 사용할 것입니다.

 

우선 downscaleImage 스크립트 파일을 임포트하고 package.json을 생성합니다.

$ git clone https://gist.github.com/45ef80c26f48084b98e50391c01048d2.git downscaleImage && \
$ cd downscaleImage && \
$ npm init

모두 기본값으로 설정하되, test 커맨드는 "karma start"를 입력합니다. 물론 나중에 설정해도 되고, test 커맨드 없이 "npx karma start"로 수행해도 됩니다. Karma를 설치하고 설정 파일을 만듭니다.

$ npm i -D karma && karma init

테스팅 프레임워크는 무엇으로 하시겠습니까? Jasmine을 택합니다. Jest는 자신의 환경이 아니면 글로벌 메소드, matchers를 지원하지 않습니다.

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Require.js가 필요합니까? 저는 Webpack으로 테스트 스크립트를 직접 번들링하려고 합니다. no라고 대답합니다.

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

테스트가 수행되는 브라우저는, 우선 Chrome만 선택합니다. 자동으로 karma-chome-launcher 라이브러리가 설치될 것입니다.

Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
>

테스트 파일의 GLOB 패턴을 입력합니다. 저는 프로젝트 루트 폴더의 .(spec|test).js 파일만을 실행하려고요.

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> ./*.@(spec|test).js
>

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>

test 커맨드를 호출할 때의 한 번만 테스트를 수행할 생각입니다. 마지막 질문에 대해 no라고 응답합니다.

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> no

Config file generated at "C:\Users\sungchuni\workspace\playground\downscaleImage\karma.conf.js".

karma.config.js가 생성되었습니다. suite 이름을 "method: downscaleImage"로 하고 간단한 테스트를 하나 작성해봅시다. 일단 downscaleImage.js가 함수를 export 하도록 수정하고, 추출된 모듈이 함수인지 확인합니다.

// ./downscaleImage.js
// asis
async function downscaleImage(
  file,
  {

// tobe
export default async function downscaleImage(
  file,
  {
// ./downscaleImage.spec.js
import downscaleImage from "./downscaleImage";

describe("fuction: downscaleImage", () => {
  it("should be function", () => {
    expect(typeof downscaleImage).toBe("function");
  });
});

이렇게는 테스트가 실행되지 않습니다. module type으로 지정되지 않은 스크립트에서는 import 구문을 사용할 수 없다는 메시지만 출력됩니다. Require.js를 사용하지 않고, Webpack을 사용하기로 했으므로 karma-webpack 플러그인을 설치하고 설정 파일을 조금 수정합니다.

npm i -D karma-webpack webpack
// karma.config.js

// ...
preprocessors: {
  "./*.@(spec|test).js": ["webpack"]
},
// ...
webpack: {
  mode: "development"
},

다시 test 커맨드를 호출하면 첫 테스트가 통과되는 것을 확인할 수 있습니다. 이제 몇 가지 spec을 더 추가합니다.

 

downscaleImage 함수는 첫째 인자가 File 인스턴스인지 확인합니다. 함수에 작성된 대로 TypeError가 던져지는지 테스트합니다.

it("should be rejected without File", async () => {
  const blob = new Blob();
  await expectAsync(downscaleImage(blob)).toBeRejectedWithError(TypeError);
});

둘째 인자는 옵션 객체입니다. 여기에 대해서도 테스트를 작성해볼 수 있을 것입니다. 예를 들면,

it("should be rejected with invalid quality option", async () => {
  const file = new File([], "");
  for (const quality of [-1, 2, NaN, Infinity]) {
    await expectAsync(downscaleImage(file, {quality})).toBeRejectedWithError(
      TypeError
    );
  }
});

이제 예외 처리 부분은 넘어가고, 몇 가지 조합의 올바른 옵션으로 테스트를 실행합니다. 우선 적당한 파일을 준비해서 Karma 서버가 이를 제공하도록 합니다. 저는 UHD 사이즈의 "sample.jpg" 파일을 준비했고 이것을 루트 폴더에 두었습니다.

// karma.config.js

// ...
files: [
  {pattern: "./*.@(spec|test).js", watched: false},
  {pattern: "./*.jpg", watched: false, included: false},
],

"/base/sample.jpg"를 fetch하여 blob => file 인스턴스를 얻어 이것으로 함수를 호출합니다.

it("should be resolved with valid File", async () => {
  const response = await fetch("/base/sample.jpg");
  const blob = await response.blob();
  const file = new File([blob], "sample.jpg", {type: "image/jpeg"});
  await expectAsync(downscaleImage(file)).toBeResolved();
});

debug 모드에서 다운스케일된 이미지를 확인할 수도 있습니다. downscaleImage 호출 후 반환 값을 가지고 화면에 적절한 이미지 태그를 생성, 삽입하고, Karma 실행 시 "--no-single-run" 파라미터를 입력하여 테스트 완료 후에도 브라우저 프로세스가 유지되도록 합니다.

it("should be resolved with targetWidth option", async () => {
  const response = await fetch("/base/sample.jpg");
  const blob = await response.blob();
  const file = new File([blob], "sample.jpg", {type: "image/jpeg"});
  const targetWidth = 640;
  const result = await downscaleImage(file, {targetWidth});
  const objectURL = URL.createObjectURL(result);
  const image = new Image();
  image.src = objectURL;
  await image.decode();
  document.body.appendChild(image);
  expect(image.width).toBe(targetWidth);
});
it("should be resolved with returnType option", async () => {
  const response = await fetch("/base/sample.jpg");
  const blob = await response.blob();
  const file = new File([blob], "sample.jpg", {type: "image/jpeg"});
  const returnType = "dataURL";
  const result = await downscaleImage(file, {returnType});
  const image = new Image();
  image.src = result;
  await image.decode();
  document.body.appendChild(image);
  expect(image.width).toBe(1280);
});

당연히 프로덕션에선 file 인스턴스를 얻는 부분을 헬퍼 함수로 추출합시다.

 

여기까지, 입니다. 생략한 부분이 많지만 벌써 테스트를 6개나 작성했습니다, 생략한 부분을 더하면 적어도 20개의 테스트를 자동화할 수 있을 것 같습니다. 이제 다음은 타입스크립트입니다. 다음에 만나요.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크