본문 바로가기

TIL(Today i learned)

[TIL-2022.09.28]jest.SpyOn, createPortal 테스트코드 작성

1.get, query, find  By의 차이점 :

테스트 코드 작성 중 get, query, find  By의 차이점에 대한 궁금증이 생김 : 

  • getBy...: Returns the matching node for a query, and throw a descriptive error if no elements match or if more than one match is found (use getAllBy instead if more than one element is expected).
  • queryBy...: Returns the matching node for a query, and return null if no elements match. This is useful for asserting an element that is not present. Throws an error if more than one match is found (use queryAllBy instead if this is OK).
  • findBy...: Returns a Promise which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, use findAllBy.

 

2. 테스트코드 작성 중 - No router instance found

HeaderBar의 경우 next/router의 useRouter를 사용한다. 해당 테스트 코드 상단에 jest.mock으로 코드를 모킹해줬는데 위와 같은 에러가 난다. 

 

컴포넌트 내에서 사용되고 있는 useRouter 부분을 제거하면 해당 에러가 사라지는데, 아무튼 이건 내가 원한 것이 아니다. 어떻게 이 에러를 해결할 수 있을까? 

 

https://github.com/vercel/next.js/discussions/13891

 

Testing next.js app - No router instance found · Discussion #13891 · vercel/next.js

Hi, I am trying to test my next.js app using Jest and React testing library. First I got an error TypeError: Cannot read property 'pathname' of null Which I solved by mocking useRouter like...

github.com

여기서 자료를 찾아보고 아래와 같이 코드를 바꿔줬더니, 에러가 사라졌다. 

const useRouter = jest.spyOn(require('next/router'), 'useRouter');
useRouter.mockImplementation(() => ({
  pathname: '/',
}));

 

jest의 spyOn은 무엇일까? 

https://jestjs.io/docs/jest-object#jestspyonobject-methodname

 

The Jest Object · Jest

The jest object is automatically in scope within every test file. The methods in the jest object help create mocks and let you control Jest's overall behavior. It can also be imported explicitly by via import from '@jest/globals'.

jestjs.io

Creates a mock function similar to jest.fn but also tracks calls to object[methodName].

이렇게 설명하고 있다. jest.fn과 비슷한 기능으로 작동하는데, 해당 함수의 method까지 추적해서 모킹하도록 하는 것 같다. 예시 코드를 보면 다음과 같다. 

const video = {
  play() {
    return true;
  },
};

module.exports = video;

 

이 코드를 보면 video라는 객체 안에, play라는 메서드를 가지고 있다. 

const video = require('./video');

afterEach(() => {
  // restore the spy created with spyOn
  jest.restoreAllMocks();
});

test('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
});

그리고 해당 video 객체 내부의 play라는 메서드를 가지고 테스트를 한다. 코드를 보면 spyOn(객체, 객체의메서드)와 같은 형식으로 모킹을 하고 있다. 그리고 해당 메서드를 호출했을 때의 결과가 예상한 것과 같은지 테스트하고 있다. 

 

 

3.css 작성 : 

Article 컴포넌트의 스타일을 수정하고, Layout 컴포넌트에서 title을 받아올 수 있도록 만들어주었다. 

수정한 사이트의 모습이다. 

 

4.createPortal은 어떻게 테스트할까? 

검색을 해보니, 

https://codesandbox.io/s/reactdomcreateportal-testing-x1icz?file=/src/App.spec.js 

 

ReactDOM.createPortal testing - CodeSandbox

ReactDOM.createPortal testing by believer using @testing-library/jest-dom, @testing-library/react, jest, react, react-dom, react-scripts

codesandbox.io

이런 자료가 있어서 참고했다. within 메서드를 사용하고 있는데, 뭐하는 메서드일까? 

within (an alias to getQueriesForElement) takes a DOM element and binds it to the raw query functions, allowing them to be used without specifying a container. It is the recommended approach for libraries built on this API and is used under the hood in React Testing Library and Vue Testing Library.

흠... 문서를 읽어도 솔직히 무슨 말인지 잘 모르겠다. 다른 query 방식이랑 뭐가 다른걸까. 

import {render, within} from '@testing-library/react'

const {getByText} = render(<MyComponent />)
const messages = getByText('messages')
const helloMessage = within(messages).getByText('hello')

아, 이해가 잘 안되었었는데, 조금 예시코드를 조금 더 읽다보니까 조금 감이 왔다. within으로 특정 node를 감싸주면, query하는 함수들을 그 특정 node에 binding 시켜주는 메서드인가보다. 

 

우선 이해는 했다. 그래서 내가 작성한 모달 컴포넌트에 대한 테스트를 어떻게 작성할 것인가? 먼저는 내가 작성할 모달 컴포넌트의 명세는 무엇인가? 

 

1.modal_root 라는 id값을 가진 div에 이 모달컴포넌트가 렌더링 되어야 한다. 

 

이것이 Modal 컴포넌트의 명세다. 특정 dom 노드에 내가 테스트 하려는 컴포넌트가 렌더링 되었는지를 테스트 하려면 어떻게해야 하는 것일까. 

 

아무튼 우선 그 특정 컴포넌틀 테스트 해야하니까, testid를 부여해주고 query해오는 방식으로 했다.  testid를 이용한 이유는 마땅한 text를 가지고 있지도 않기 때문에 그냥 testid를 부여하고 가져오기로 했다. 

 

아.. 근데 이 testid 값을 가진 컴포넌트를 찾지못하고 있다. 아무래도 화면에 나타나질 않는 것 같은데, 아래가 나의 모달 컴포넌트의 모습이다. 이코드를 잘 보면 useEffect 내부에 getElementById를 통해서 modal_root를 가져오고 있는데, 아무래도 jestdom 환경에서는 저 modal_root를 가진 친구가 없다보니까 화면에 로드하지를 못하는 것 같다. 

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import styled from '@emotion/styled';

...

export const Modal = ({ children }: ModalProps) => {
  const [container, setContainer] = useState<HTMLElement | null>(null);

  useEffect(() => {
    setContainer(document.getElementById('modal_root'));
  }, []);

  if (!container) return null;

  return ReactDOM.createPortal(
    <>
      <Overlay />
      <Box data-testid="modal">{children}</Box>
    </>,
    container,
  );
};

휴... 테스트 코드를 작성하는 것이 산넘어 산이다😵

머리를 좀 식히면서 다른 컴포넌트 먼저 작성하고, 다시 돌아와서 Modal 부분 컴포넌트를 어떻게 작성할 것인지를 생각해보자. 

5.jest-dom 전역적으로 설정해주기 

 

그런데, 이거 dom 테스트를 진행할 때마다 파일 상단에 

import '@testing-library/jest-dom';

이렇게 작성해줘야하는 것이 번거롭다. 이것을 전역적으로 사용할 수 있는 방법은 없는걸까? 검색을 해보니 그런 방법은 당연히 존재했다. 

jest.config.js파일에 

const nextJest = require('next/jest');

const createJestConfig = nextJest({
  dir: './',
});

const customJestConfig = {
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jest-environment-jsdom',
  setupFilesAfterEnv: ['<rootDir>/setupTests.js'], >>>> 이 부분!!
};

module.exports = createJestConfig(customJestConfig);

이렇게 넣어줬다. 나는 현재 프로젝트의 최상단에 setupTests.js 파일을 위치해 놓았으므로 저렇게 디렉토리를 적어주었다. 그리고 setupTests.js 파일에는 

import '@testing-library/jest-dom/extend-expect';

이렇게 적어주면 된다. 더 이상 매번 jest dom 테스트를 할 때, 파일 상단에 jsdom 환경으로 하겠다고 설정해주지 않아도 된다. 

 

 

 

6.TagInput 컴포넌트 작성 :  

TagInput하면 가장 먼저 생각나는 레퍼런스는 velog에서의 TagInput이다. 그래서 소스코드를 다운 받아서 확인해보는 중이었다. 먼저는 코드를 읽어보면서 이해해보려고 했다. 내가 알지 못했던 몇가지 신기한 기능들이 있어서 해당 기능들을 잠깐 찾아봤다. 

 

먼저는 contenteditable라는 속성이다. 특정 문서를 사용자가 edit할 수 있게 해주는 속성이다. 

https://developer.mozilla.org/ko/docs/Web/HTML/Global_attributes/contenteditable

 

contenteditable - HTML: Hypertext Markup Language | MDN

contenteditable 전역 특성은 사용자가 요소를 편집할 수 있는지 나타내는 열거형 특성입니다.

developer.mozilla.org

 

다음으로 발견한 속성은 tabindex이다. tabindex="0"을 명시해주면 tab 키보드를 눌렀을 때 접근가능한 위치가 된다. 

https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex

 

tabindex - HTML: HyperText Markup Language | MDN

The tabindex global attribute indicates that its element can be focused, and where it participates in sequential keyboard navigation (usually with the Tab key, hence the name).

developer.mozilla.org

https://www.tpgi.com/using-the-tabindex-attribute/

 

Using the tabindex attribute - TPGi

The HTML tabindex attribute is used to manage keyboard focus. Used wisely, it can effectively handle focus within web...

www.tpgi.com

 

주의 사항이 있다면, 접근성 측면에서 봤을 때 대화형 엘리먼트에만 tabindex를 사용하는 것이 좋다. 대화형 엘리먼트가 무엇인지는 여기에 나와있다. 

https://html.spec.whatwg.org/multipage/dom.html#interactive-content

 

HTML Standard

 

html.spec.whatwg.org

 

혹시 다른 참고할 소스코드는 없을까해서 찾아봤는데, 생각보다 많았다. 어쩌다보니 생각보다 다양한 ui 라이브러리를 찾게 되었다. 

 

https://evergreen.segment.com/

 

Evergreen

Evergreen is a React UI Framework for building ambitious products on the web. Made by Segment in San Francisco, CA.

evergreen.segment.com

 

https://buefy.org/documentation/taginput/

 

Taginput | Buefy

A simple tag input field that can have autocomplete functionality

buefy.org

 

https://bootstrap-tagsinput.github.io/bootstrap-tagsinput/examples/

 

Bootstrap Tags Input

add Adds a tag $('input').tagsinput('add', 'some tag'); $('input').tagsinput('add', { id: 1, text: 'some tag' }); Optionally, you can pass a 3rd parameter (object or value) to the add method to gain more control over the process. The parameter is exposed i

bootstrap-tagsinput.github.io

역시 라이브러리들의 소스코드를 확인해보니 생각이상으로 복잡하다. 그만큼 많은 것들을 고려하고 작성하는 것 같다는 생각이 든다. 그냥 이 라이브러리들을 사용할까 싶다. tagInput에 그렇게 많은 힘을 쏟을 필요는 없지 않을까? 

 

아 그리고 evergreen이라는 서비스에서 오류를 찾았다. 한글로 입력하는 경우 마지막에 ㅇ이 들어가는 글자를 입력하는 경우 하나 더 생성된다. 이 에러와 관련해서 오류를 수정하고 pr을 올리면 나도 오픈소스 기여자..? 

 

 

7.맥북에 커피 쏟음 : 

하... 맥북에 커피를 쏟았다. 심지어 크림이 있는 커피였기 때문에 매우 끈적끈적한 것들이 키보드 안으로 들어갔다. 다행히 나는 애플케어를 들어뒀기에 망정이지, 안들어뒀으면 한 순간에 200만원돈이 날아가버릴 번 했다. 수리센터에 가니 이것저것 상담을 해줬고, 아무래도 액체였고 여기저기에 액체가 스며들어가서 내부 부품들을 거의다 교체해야하는 상황이었다. 따지고보면 외부 철판 빼고 내부는 전부다 교체하게 된 것이다. 다행히 애플케어를 들어두면 아무리 수리를 많이해도 가격은 37만원 이상으로 올라가지 않는다. 이번에 견적은 거의 230만원 정도 나온 것 같은데, 덕분에 37만원만에 수리할 수 있게 되었다. 따지고 보면 37만원으로 새 노트북으로 교체하게 된 것이다... (어떻게 보면 좋은 일일지도...굳이 새 노트북이 필요하진 않았지만ㅜㅜㅜ) 

아무튼 근데 지금 제품 재고가 없어서 입고요청을 해야하고, 입고가 될 때까지 노트북을 일단 사용하기로 했다. 노트북을 사용하다가 입고 연락이오면 노트북을 가져다주고 수리하기로. 때문에, 입고연락이 오기 전에 프로젝트를 열심히 달려야한다. 노트북을 맡기고 난 이후로는 이론공부에 몰입해야할 것 같다..^^