티스토리 뷰
"단위 테스트를 어디에 쓸까?"하고 정말로 그렇게 묻는다면, 정말로 정말로는 그거 없이도 프로그램은 돌아간다고 말을 할 수가 있을 것입니다. 테스트는 그런 자리에서 의미가 있는 것 같습니다, 어플리케이션의 목적에 기여하는 자리는 아니고 프로젝트 정도에 기여하는.
몇 가지 리팩터링을 계획하고 있습니다. downscaleImage를 타입스크립트로 작성하여 매개변수를 어떻게 전달해야하는지, 반환값은 어떤 레이아웃인지 알기 쉽도록 할 예정입니다. 그리고 타입스크립트로 변환한 이 함수 내부의 조건 분기를 더 작은 함수로 나누어 논리 흐름을 간결하게 합니다. 이 작업들이 완료되면 트랜스파일러 없이도 함수가 사용 가능하도록 레거시 자바스크립트 코드로 다시 작성합니다. (바벨의 결과물은 휴먼리더블하진 않습니다.)
타입스크립트 변환
함수 분할
레거시 지원
매 작업 절차마다 산출된 프로그램(함수)은 원래의 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개의 테스트를 자동화할 수 있을 것 같습니다. 이제 다음은 타입스크립트입니다. 다음에 만나요.
'Note' 카테고리의 다른 글
왜 나의 flexbox는 white-space에게 지기만 하는가? (0) | 2020.10.08 |
---|---|
자바스크립트로 작성된 함수와 테스트를 타입스크립트로 작성하기 (0) | 2020.09.16 |
Jest, Jasmine의 matchers 메소드 비교, 대조 (0) | 2020.08.31 |
캔버스를 활용한 이미지 다운스케일링 시 앨리어싱 문제 (0) | 2020.08.02 |
캔버스를 활용한 이미지 다운스케일링 시 안티앨리어싱 적용 (0) | 2020.08.01 |