Svelte에서 테스트 환경 설정 및 유닛 테스트 작성

Dev-Yuns
Svelte Seoul
Published in
11 min readJan 11, 2022

--

Photo by Alex Kondratiev on Unsplash

해당 포스트에서는 테스트에 관한 복잡한 방법론과 철학보다는 코드를 통해 실질적으로 어떻게 테스트를 수행하고 코드 커버리지를 늘려 나갈 것인가에 대해 말하고자 합니다.

프론트엔드에서 테스트 순서

복잡한 방법론을 차치한다면, 프론트엔드에서 테스트는 다음 스텝으로 정의할 수 있을 것 같습니다.

  1. 테스트 환경을 설정한다.
  2. 테스트 대상에 필요한 테스트 케이스를 정의한다.
  3. 각 테스트 케이스 내에서 테스트할 컴포넌트, 함수 혹은 모듈을 있는 그대로 혹은 mocking하여 가져온다.
  4. 가져온 대상에 원하는 동작을 일으킨다.
  • 대상이 컴포넌트면 props를 넘겨줍니다.
  • 대상이 함수라면 테스트 케이스에 맞는 인자를 넘겨줍니다.
  • 반응성 테스트를 위해서는 임의로 이벤트를 발생시켜줍니다.

5. Matcher함수를 활용해서 기대값 혹은 기대하는 동작과 일치하는지 확인한다.

이제 각 스텝 별로 상세히 살펴보겠습니다.

설치 패키지

먼저 테스트 환경을 설정하기 위해 설치할 패키지들을 살펴보겠습니다. 제가 생각할 때 최소한의 구성만 담았습니다. 하단의 패키지들을 선호하는 패키지 매니저로 설치해주세요. svelte의 경우에는 대부분의 패키지를 devDependencies 에 설치합니다.

yarn add -D jest babel-jest svelte-jester ts-jest @testing-library/svelte @testing-library/jest-dom @testing-library/user-event

jest

Jest는 자바스크립트의 테스트 프레임워크로 test runner, matcher, mocking 등 테스트에 필요한 대부분의 기능을 제공해주고 있습니다. 앞으로 작성할 테스트 코드의 가장 핵심 패키지이며 이후의 패키지는 jest의 기능을 확장해주는 역할을 한다고 생각하시면 됩니다.

babel-jest

babel 설정을 jest에 로드 해주는 역할을 합니다. 최근의 프론트엔드 프로젝트들은 대부분 바벨을 사용하여 최신 자바스크립트 코드를 사용하고 있기 때문에 jest가 해석할 수 있도록 코드를 변환해줄 필요가 있습니다.

svelte-jester

여기에서는 svelte를 사용했기 때문에 jest가 svelte 모듈을 해석할 수 있도록 변환해주는 패키지입니다. 만약 리액트를 사용했다면 jsx를 변환하기 위해서 react-jester 가 사용될 것입니다.

ts-jest

프로젝트에서 타입스크립트를 사용하고 있기 때문에 타입스크립트 파일을 해석하기 위해 사용했습니다.

testing-library/svelte

프론트엔드에서는 테스트할 대상이 컴포넌트일 경우가 많습니다. 컴포넌트는 사용하는 프레임워크에 따라 일반 함수처럼 가져오기 힘들기 때문에 보통 별도의 testing-library 를 사용합니다. svelte 외에도 각 프레임워크 별 testing-library가 존재합니다.

testing-library/jest-dom

jest에서 기본적으로 제공하는 matcher 함수의 기능을 확장해주는 라이브러리입니다. dom과 관련된 값들을 확인할 때 편리한 함수들을 제공 해줍니다.

testing-library/user-event

반응성 테스트를 위해서 testing-library에서 기본적으로 제공하는 fireEvent 를 사용할 수도 있지만 여기서는 user-event 패키지를 사용했습니다. testing-library 공식 문서에서도 유저의 동작을 시뮬레이션 할 때는 user-event 패키지를 사용할 것을 권장하고 있습니다. fireEvent 는 유저에게 실제로 노출되지 않을 이벤트도 dispatch해주지만 user-event를 사용하면 정확히 유저가 실제 환경에서 시도할 이벤트들을 디스패치할 수 있습니다. 이를 통해 개별 이벤트를 테스트할 때 잡을 수 없는 버그를 잡을 수도 있습니다. 개인적인 생각으로도 테스트 케이스를 잘 정의 했다면 내부의 이벤트를 개별 테스트하는 것보다 유저에게 노출된 이벤트를 테스트하는게 맞다고 생각합니다. 참고

테스트 환경 설정하기

패키지들을 모두 설치했다면 이제 jest.config.js 파일을 만들어 보겠습니다. jest는 config옵션들을 package.json 파일 혹은 프로젝트 최상단의 jest.config.js 파일에 두도록 하고 있습니다. 설정들을 하나씩 살펴보겠습니다.

transform

Jest는 앞에서 언급했듯이 코드를 자바스크립트로 실행하기 때문에 자바스크립트 외에 타입스크립트, 리액트의 jsx, 타입스트립트, 스벨트 등의 코드를 자바스크립트로 변환시켜주어야 합니다. 이때 transform 옵션을 사용합니다.

Jest는 자동으로 babel-jest 를 로드 해줍니다. babel-jest 는 기본적으로 타입스크립트 파일을 변환 해주지만, 타입을 확인하지 않으므로 여기서는 ts-jest 를 사용해주었습니다. 또한 .svelte 확장자 파일을 위해 svelte-jester 를 넣어줍니다.

transformIgnorePatterns

트랜스파일을 시도하기 전에 패턴에 매칭되는 파일들을 제외 해줍니다. 기본적으로 node_modules 는 트랜스파일에서 제외됩니다. 그러나 종종 서드파티 라이브러리 중에서 (특히 타입스크립트나 리액트 네이티브 프로젝트) 변환되지 않은 상태로 배포되는 경우가 있습니다. 이 경우에는 해당 파일을 트랜스파일 하지 않으면 jest가 이해할 수 없습니다. 따라서 필요한 경우 트랜스파일 되도록 명시해줄 필요가 있습니다.

// 아래 정규 표현식은 ()안의 패키지를 제외한 결과를 반환해줍니다.
transformIgnorePatterns: [
"node_modules/(?!(react-native|my-project)/)"
]

moduleNameMapper

정규표현식에 매칭되는 모듈들을 stub으로 대체해주는 역할을 합니다.

testRegex

테스트할 파일을 찾기위한 정규표현식입니다. 굳이 명시하지 않아도 __tests__ 폴더와 .test|spec.ts 와 같은 형식의 파일을 찾기 위한 정규표현식이 기본적으로 들어가 있습니다.

modulePathIgnorePatterns

정규표현식에 매칭되는 모듈이 모듈 로더에서 require되지 않도록 해줍니다.

testEnvironment

테스트 환경과 관련된 옵션입니다. 기본은 node입니다. 프론트엔드에서 테스트를 진행하기 때문에 웹 환경을 위해 jsdom 을 명시 해줍니다.

globals

전체 테스트 환경에서 사용할 전역 변수를 설정합니다.

setupFilesAfterEnv

테스트 코드가 실행되기 전 실행할 파일들을 명시해줄 수 있습니다. 저는 주로 테스트 환경에서 전역으로 사용할 모듈을 로드하거나, 모듈을 모킹할 때jestSetup.ts 파일에 명시하는 편입니다. test/jestSetup.ts 파일에서 jest-dom 패키지를 import 해주고 있습니다.

테스트 케이스 정의하기

환경 설정이 끝났다면 이제 테스트 코드를 작성할 차례입니다. 스타일에 따라 다르지만 __tests__ 폴더를 만들어서 테스트 코드 파일을 모아두거나 테스트할 컴포넌트와 테스트 코드를 같은 폴더에 함께 두는게 제일 흔한 것 같습니다.

저는 edit-text.test.ts파일을 만들었습니다. 이제 테스트 케이스를 정의할 차례입니다. 저는 테스트하고 싶은 동작과 함께 묶을 테스트 케이스들을 미리 정리해보는 편입니다.

it 문은 test 문을 좀 더 간단하게 표현한 것이고 동작은 동일합니다. describe 메서드는 테스트의 scope 를 정의하기 위해 사용합니다. before*after* 메서드를 사용할 경우 describe 메서드를 기준으로 동작하게 됩니다. 테스트 문의 범위와 관련해서는 Kent C. Dodds포스트를 참고하시면 좋을 것 같습니다.

테스트 코드 작성하기

이제 각 테스트 문에서 사용할 대상을 가져와야합니다. it문 상단에서 테스트할 컴포넌트를 렌더링한 후, 쿼리 메서드를 통해 원하는 element를 가져옵니다. 쿼리 메서드는 다양한 형태로 존재하며 상황에 맞게 사용할 수 있습니다. 어떤 경우에 어떤 쿼리문을 사용할지는 계속 사용하다보면 자연스럽게 익숙해지는 것 같습니다.

쿼리 메서드의 종류

  • getBy… : 쿼리에 매칭되는 노드를 리턴합니다. 하나 이상의 노드가 발견되거나(getByAll은 제외) 매칭되는 요소가 없을 경우 error를 일으킵니다.
  • queryBy… : 쿼리에 매칭되는 노드를 리턴한다. 매칭되는 노드가 없으면 null을 반환합니다. 요소가 없다는 것을 확인할 때 유용합니다.
  • findBy… : 주어진 쿼리와 매칭되는 노드가 발견되었을 때 resolve되는 프로미스를 반환합니다. 매칭되는 노드가 없거나 하나 이상의 노드가 주어진 타임아웃 이후에 발견될 때 reject됩니다.

UI 테스트

render 메서드의 첫 번째 인자로 Edittext 컴포넌트를 넣어주고, 두 번째 인자로 props 를 넣어줍니다. 이렇게 원하는 형태의 컴포넌트를 랜더링해줄 수 있습니다. 예시에서는 객체 디스트럭처링을 통해 getByRole 메서드를 가져오고 있습니다.

getByRole('textbox')Edittext 내에서 input 요소를 찾아줍니다. 현재 테스트는 inputplaceholder 가 잘 props로 잘 주입되었는지 확인하고 있기 때문에 getAttribute('placeholder') 를 통해 placeholder 요소를 가져옵니다. expect 문은 인자로 받은 요소를 포함하는 객체를 반환해주며 연결되는 matcher 메서드에 넘겨줍니다. toEqual 은 넘겨받은 값과 인자로 받은 값과 일치하는지 확인합니다.

단순한 코드 구성이지만 테스트할 컴포넌트를 렌더링하고, 원하는 element를 가져온 후, matcher 함수를 통해 비교하는 테스트 동작을 모두 포함하고 있습니다.여기에서 테스트 케이스가 복잡해지만 요소를 가져오거나 원하는 조건을 만들어내는데 코드가 조금 더 길어질 수 있습니다.

위 코드는 jest의 matcher메서드를 확장시켜주는 jest-dom 을 통해 더 간단히 표현할 수 있습니다.

expect(getByRole('textbox')).toHaveAttribute('placeholder', 'test');

유저 이벤트 테스트

이번에는 유저 이벤트 테스트를 진행해 보겠습니다. 저는 setUpUser 라는 함수를 미리 testUtils 라는 파일에 정의해두었습니다. userEvent.setup() 함수는 항상 컴포넌트를 렌더링 하기 전에 실행되어야 하기 때문에 다음과 같이 만들었습니다.

setUpUser함수를 활용한 테스트 코드는 아래와 같습니다.

setUpUser 에 컴포넌트를 넘겨주고 user 를 가져옵니다. 그리고 유저와 상호작용할 대상이 되는 input 을 가져옵니다. 이번에는 getByAltText 를 통해 input 을 가져왔습니다. 이후에 type 메서드를 통해 유저의 입력 이벤트를 비동기로 실행 해줍니다. 마지막으로 expect 문의 matcher 메서드를 통해 값을 확인 해줍니다.

만약 유저 이벤트를 여러 번 실행한다면 clear 메서드를 통해 내용을 지워줘야할 수도 있습니다. 한 테스트 케이스 내에서는 렌더링이 다시 진행되지 않기 때문에 명시적으로 clear 동작을 수행하지 않으면 변경사항이 그대로 남아있다는 점을 유의해야 합니다.

컴포넌트 이벤트 핸들러 테스트

마지막으로 컴포넌트에 바이딩되어 있는 핸들러를 어떻게 테스트 하는지 알아보겠습니다. 테스트를 위해서 on:click이벤트가 바인드 되어 있는 button 컴포넌트를 활용했습니다.

코드가 약간 더 복잡해졌습니다. 앞에서 살펴본 것과 마찬가지로 setUpUser 를 통해 getByRole , user 를 가져오고 추가로 component 를 하나 더 가져옵니다. componentButton 을 가리키고 있습니다. 먼저 내부의 buttongetByRole 을 통해 가져오고, on:click 이벤트에 바인딩 해줄 mock 함수를 선언해줍니다. 이후에 $on 메서드를 통해 component 에 핸들러 이름와 함수를 바인딩해줍니다. 이제 on:click 이벤트가 발생하면 mockFn 함수가 호출될 것입니다. toHaveBeenCalledTimes(1) 메서드를 통해 함수가 정확히 1번 호출되었는지 확인합니다.

결론

간단한 테스트 코드를 만들어보면서 테스트 셋업부터 실행까지 전 과정을 살펴보았습니다. 테스트 대상을 가져오고, 변형을 일으키고 그 값을 확인한다는 기본 동작은 모두 동일하지만 워낙 다양한 메서드들이 존재하기 때문에 결국 많은 케이스를 통해서 익숙해지는 것이 중요한 것 같습니다.

다음 포스트에서는 모듈이나 함수를 모킹하는 등 보다 복잡한 케이스를 예로 들어서 테스트 코드를 작성해보겠습니다. 감사합니다.

--

--