[KDT]FCFE - 8주4일 React Testing

Keunyeong Lee·2022년 1월 13일
0
post-thumbnail

React

React Testing

Javascript Unit Test & Jest 사용하기

  • 통합테스트에 비해 빠르고 쉽다
  • 통합테스트를 진행하기 전에 문제를 찾아낼 수 있다.
  • 테스트 코드가 살아있는 명세가 된다.( 테스트를 읽고 어떻게 동작하는지도 예측 가능하다.)

jest

  • 현재 가장 핫한 테스트 도구

  • Easy Setup

  • Instatnt Feddback ( 고친 파일만 빠르게 테스트 다시 해주는 기능 등.)

  • Snapshot Testing ( 컴포넌트 테스트에 중요한 역할을 하는 스냅샷 )

  • CRA 에는 jest 가 내장되어있다.

  • npm init -y 하여 테스트해보기

npm i jest

  • package.json
"script" : {
  "test" : "jest"
}
  • example.test.js
test("adds 1+2 to equal 3", ()=>{
  expect(1+2).toBe(3);
})

npm test

  • 여러가지 테스트 (deiscribe)
describe("expect test", ()=>{
  it("37 to equal 37", ()={
    expect(37).toBe(37);
  });
  it("{age:39} to equal {age:39}", ()={
    expect({age:39}).toEqual({age:39});
  });
  it(".toHaveLength", ()={
    expect("hello").toHaveLength(5);
  });
  it(".toHaveProperty", ()={
    expect({name: "Mark"}).toHaveProperty("name");
    expect({name: "Mark"}).toHaveProperty("name","Mark");
  });
  it(".toBeDefined", ()={
    expect({name: "Mark"}.name).toBeDefined();
    expect({name: "Mark"}.age).toBeDefined(); //Error
  });
  it(".toBeFalsy", ()={
    expect(false).toBeFalsy();
    expect(0).toBeFalsy();
    expect('').toBeFalsy();
    expect(null).toBeFalsy();
    expect(undefined).toBeFalsy();
    expect(NaN).toBeFalsy();
  });
  it(".toBeGreaterThan", ()={
    expect(10).toBeGreaterThan(9);
  });
  it(".toBeGreaterThanOrEqual", ()={
    expect(10).toBeGreaterThanOrEqual(10);
  });
  it(".toBeInstanceOf", ()={
    class Foo {}
    expect(new Foo()).toBeInstanceOf(Foo);
  });
});
  • 원시값이 아닌 객체를 비교하기 위해서는 toBe가 아닌 toEqual 을 사용해야 한다.

  • .not.to 로 반대경우를 테스트 할 수 있다.

  • 테스트 계속 켜놓기

npx jest --watchAll

  • async test with done callback
describe('use async test', ()=>{
  it('setTimeout without done', (done)=>{
    setTimeout(()=>{
      expect(37).toBe(36);
      done();
    },1000)
  })
})
  • it 호출 함수에 인자로 done 을 넣고 setTimeout 실행 함수 안에 done()을 넣으면 done() 함수가 실행되기 전에는 test를 끝내지 않는다.

  • async test with promise

describe('use async test', ()=>{
  it('promise then', ()=>{
    function p() {
      return new Promise (resolve =>{
        setTimeout(()=>{
          resolve(37);
        }, 1000)
      });
    }
    return p().then(data => expect(data).toBe(37));
  });
  
  it('promise catch', ()=>{
    function p() {
      return new Promise ((resolve,reject) =>{
        setTimeout(()=>{
          resolve(new Error('error'));
        }, 1000)
      });
    }
    return p().catch(e => expect(e).toBeInstanceOf(Error));
  });
})
describe('use async test', ()=>{
  it('promise .resolves', ()=>{
    function p() {
      return new Promise (resolve =>{
        setTimeout(()=>{
          resolve(37);
        }, 1000)
      });
    }
    return expect(p()).resolves.toBe(37);
  });
  
  it('promise .rejects', ()=>{
    function p() {
      return new Promise ((resolve, reject) =>{
        setTimeout(()=>{
          resolve(new Error('error'));
        }, 1000)
      });
    }
    return expect(p()).rejects.toBeInstanceOf(Error);
  });
})
  • 가장 많이 사용하는 방식
describe('use async test', ()=>{
  it('async-await', async ()=>{
    function p() {
      return new Promise (resolve =>{
        setTimeout(()=>{
          resolve(37);
        }, 1000)
      });
    }
    
    const data = await p();
    return expect(data).toBe(37);
  });
  
  it('async-await, catch', async()=>{
    function p() {
      return new Promise ((resolve, reject) =>{
        setTimeout(()=>{
          resolve(new Error('error'));
        }, 1000)
      });
    }
    
    try {
      await p();
    } catch (error) {
      expect(error).toBeInstanceOf(Error);
    }
  });
})

React Component Test

  • test 할 component
import { useEffect, useRef, useState } from 'react';

const BUTTON_TEXT = {
  NORMAL: '버튼이 눌리지 않았다.',
  CLICKED: '버튼이 방금 눌렸다.',
};

export default function Button () {
  const [message, setMessage] = useState(BUTTON_TEXT.NORMAL);
  const timer = useRef();
  
  useEffect(()=>{
    return (()=>{
      if(timer.current){
        clearTimeout(timer.current)
      }
    })
  }, [])
  
  return (
    <div>
      <button onClick={click} disabled={message === BUTTON_TEXT.CLICKED} >button</button>
      <p>{message}</p>
    </div>
  )
  
  function click() {
    setMessage(BUTTON_TEXT.CLICKED);
    timer.current = setTimeout(()=>{
      setMessage(BUTTON_TEXT.NORMAL);
    },5000)
    
  }
}
describe("Button 컴포넌트 (@testing-library/react)", ()=>{
  it("컴포넌트가 정상적으로 생성된다.", ()=>{
    const button = render(<Button/>);
    expect(button).not.toBe(null);
  });
  it('"button" 이라고 쓰여있는 엘리먼트는 HTMLButtonElement 이다', ()=>{
    const { getByText } = render(<Button />);
    
    const buttonElement = getByText("button");
    
    expect(buttonElement).toBeInstanceOf(HTMLButtonElement);
  });
  it('버튼을 클릭하면, p 태그 안에 "버튼이 방금 눌렸다." 라고 쓰여진다.', ()=>{
    const { getByText } = render(<Button />);
    
    const buttonElement = getByText("button");
    
    fireEvent.click(buttonElement);
    
    const p = getByText('버튼이 방금 눌렸다.');
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
  it('버튼을 클릭하기 전에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.', ()=>{
    const { getByText } = render(<Button />);
    
    const p = getByText('버튼이 눌리지 않았다.');
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
  it('버튼을 클릭하고 5초 뒤에는, p 태그 안에 "버튼이 눌리지 않았다." 라고 쓰여진다.', ()=>{
    
    jest.useFackTimers();
    
    const { getByText } = render(<Button />);
    
    fireEvent.click(buttonElement);
    
    act(()=>{
      // 5 초 흐른다.
      jest.advanceTimersByTime(5000);
    })
    
    const p = getByText('버튼이 눌리지 않았다.');
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
  it('버튼을 클릭하면 5초동안 버튼이 비활성화 된다.', ()=>{
    
    jest.useFackTimers();
    
    const { getByText } = render(<Button />);
    
    fireEvent.click(buttonElement);
    
    // 비 활성화
    // expect(buttonElement.disabled).toBeTruthy();
    expect(buttonElement).toBeDisabled();
    
    act(()=>{
      // 5 초 흐른다.
      jest.advanceTimersByTime(5000);
    })
    
    // 활성화
    // expect(buttonElement.disabled).toBeFalsy();
    expect(buttonElement).not.toBeDisabled();
    
    const p = getByText('버튼이 눌리지 않았다.');
    expect(p).not.toBeNull();
    expect(p).toBeInstanceOf(HTMLParagraphElement);
  });
});
  • AAA 패턴
    : act 블럭에서 값이 변경 되면 act() 안에서 실행 시켜준다.

React Advanced

Optimizing Performance

: 필요할 때만 랜더!

Reconciliation

  • 랜더 전후의 일치 여부를 판단하는 규칙

  • 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.

  • 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.

비교하여 랜더를 하지 않도록 하기

  • 클래스형 컴포넌트에서 React.component 일 때

: shouldComponentUpdate를 활용하여 인자로 들어오는 previousProp와 this.props 를 비교하여 막는다.

  • 클래스형 컴포넌트에서 React.PureComponent 를 사용하면 위와 같이 이전과 비교하여 다를때만 랜더하도록 한다.

  • props 를 비교하여 랜더를 결정하기 때문에 props 에 함수를 넣을때는 선언된 함수를 변수값으로 넣어야 매번 새롭게 함수가 선언되어 랜더가 일어나는것을 막을 수 있다.

  • 함수형 컴포넌트에서 막기 (meme() 사용)

const Person = React.memo(({name, age})=>{
  console.log("Person render");
  return (
    <div>
      {name} / {age}
    </div>
  );
});

: 클래스형 컴포넌트의 React.PureComponent 처럼 이전 props 와 비교하여 변경사항이 없으면 랜더를 막는다.

  • 함수형 컴포넌트에서 props 에 실행시킬 함수가 들어가는 경우 랜더를 막기 위해서 useCallback()을 사용한다.
const Person = React.memo(({name, age})=>{
  console.log("Person render");
  
  const toPersonClick = React.useCallback(()=>{},[]);
  
  return (
    <div onClick={toPersonClick}>
      {name} / {age}
    </div>
  );
});

: useCallback() 함수의 두번째 인자인 의존성 부분을 빈 배열로 넣게되면 최소 실행시에만 첫번째 인자로 들어온 내용을 생성해준다. 의존성을 설정해줄 경우 의존하는 부분이 변경될때 새롭게 생성한다.

createPortals

: 리엑트의 랜더 영역에 있지 않은 바깥쪽의 DOM 에 리액트 컴포넌트를 랜더할 수 있도록 한다.

  • Modal.jsx
import ReactDOM from "react-dom";

const Modal = ({children})=>{
  return (
    ReactDOM.createPortal(children, document.querySelector("#modal"))
  );
}

export default Modal;
function App(){
  const [visible, setVisible] = useState(false);
  
  const open = () => {
    setVisible(true)
  }
  
  const close = () => {
    setVisible(false)
  }
  
  return (
    <div>
      <button onClick={open}>open</button>
      {visible && <Modal>
        <div style={
            { width:'100vw', 
              height: '100vh', 
              background: 'rgba(0,0,0,0.5)'}} 
            onClick={close}>Hello</div>
      </Modal>}
    </div>
  )
}

export default App;
profile
🏃🏽 동적인 개발자

0개의 댓글