logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    6장: 비동기 코드 단위 테스트

    이미지 보기

    6장: 비동기 코드 단위 테스트

    비동기 코드를 테스트하는 여러 방법들

    • 25.03.17 작성

    • 읽는 데 9

    TOC

    참고

    본 내용은 단위 테스트의 기술을 읽고 정리한 내용입니다.
    책의 내용과 함께 개인적인 의견과 생각, 학습을 담아 작성하였습니다.

    비동기 데이터 가져오기

    작업 기다리기

    done 콜백의 역할

    • jest에게 테스트의 (성공적) 종료를 알려주는 역할
    • 콜백을 사용하는 케이스
    test('async task test (using done)', (done) => {
      setTimeout(() => {
        expect(true).toBe(true);
        done();
      }, 3000);
    });
    

    jest는 done이 호출될 때까지 기다려 비동기 작업이 완료된 시점으로 인식

    done을 사용하지 않는 경우

    test('sync task test', () => {
      const result = 1 + 1;
      expect(result).toBe(2);
    });
    

    타임아웃 설정

    test('async task test (timeout for 10 seconds)', (done) => {
      jest.setTimeout(10000);
      setTimeout(() => {
        expect(true).toBe(true);
        done();
      }, 6000);
    });
    
    • 타임아웃 시간을 10초로 설정
    • 6초 후에 done을 호출하여 테스트 성공

    async/await 사용

    콜백 쓰는 것보다 더 깔끔해진다.

    // 콜백 사용
    test('NETWORK REQUIRED (callback)', (done) => {
      samples.funcAsyncAwait().then((result) => {
        expect(result.success).toBe(true);
        expect(result.status).toBe('ok');
        done();
      });
    });
    
    // async/await 사용
    test('NETWORK REQUIRED (async/await)', async () => {
      const result = await samples.funcAsyncAwait();
      expect(result.success).toBe(true);
      expect(result.status).toBe('ok');
    });
    

    통합 테스트의 어려움

    [참고] 함수의 호출이 비동기적이기에, 저자는 이를 통합 테스트라고 부른다.

    • 긴 실행 시간: 단위 테스트에 비해 느린 실행 속도
    • 불안정성: 환경에 따라 달라질 수 있는 실행 시간과 실패/성공 결과
    • 테스트와는 관계없는 코드나 환경 검증
    • 파악하는 데 더 많은 시간 소요
    • (부정적인) 테스트 상황 재현의 어려움
    • 신뢰하기 어려운 결과 : 테스트 실패가 외부인지 내부인지 알기 어려움

    코드를 단위 테스트에 적합하게 만들기

    진입점 분리 패턴

    프로덕션 코드에서 순수 로직 부분을 별도의 함수로 분리하여 그 함수를 테스트의 시작점으로 사용하는 패턴

    콜백을 새로운 함수로 분리하여 순수 논리 작업 단위의 진입점으로 사용

    어댑터 분리 패턴

    본질적으로 비동기적인 요소를 분리하고 이를 추상화하여 동기적인 요소로 대체할 수 있게 하는 패턴

    • 비동기 코드를 의존성처럼 여기는 전략
    • 비동기 코드를 대체하고 싶은 코드 취급
    어댑터의 유형은 다양
    • 모듈형 : 전체 모듈이나 파일을 스텁으로 만들어 특정 함수를 대체
    • 함수형 : 시스템에 함수나 값을 주입하는 경우. 주입된 값을 테스트에서 스텁으로 대체 가능
    • 객체 지향형 : 프로덕션 코드에서 인터페이스를 사용하고, 테스트에서 해당 인터페이스를 구현한 스텁을 만드는 경우

    타이머 다루기

    몽키패칭으로 타이머를 스텁으로 만들기

    • 몽키 패칭: 프로그램이 실행 중인 동안 시스템 소프트웨어를 로컬에서 확장하거나 수정하는 방법
    const Samples = require('./timing-samples');
    
    describe('monkey patching', () => {
      let originalTimeout;
      // 전역 객체에 원래 타이머 함수를 보관
      beforeEach(() => (originalTimeout = setTimeout));
      // 각 테스트 종료 시 몽키 패칭된 타이머 함수를 복원
      afterEach(() => (setTimeout = originalTimeout));
    
      test('calculate1', () => {
        setTimeout = (callback, ms) => callback(); // 타이머 함수 몽키 패칭(즉시 실행)
        Samples.calculate1(1, 2, (result) => {
          expect(result).toBe(3);
        });
      });
    });
    
    • 단점 1. 보일러 플레이트 코드가 많이 필요
    • 단점 2. 몽키 패칭된 함수를 되돌려야 하는데 자주 놓친다.

    jest로 setTimeout 대체

    jest는 타이머 함수를 처리하기 위한 기능을 제공한다.
    • jest.useFakeTimers() : 다양한 타이머 함수를 스텁으로 대체
    • jest.resetAllTimers() : 모든 가짜 타이머를 진짜 타이머로 재설정
    • jest.advanceTimersToNextTimer() : 가짜 타이머를 작동시켜 콜백 실행
    describe('caclulate1 - with jest', () => {
      beforeEach(() => jest.useFakeTimers());
      beforeEach(() => jest.clearAllTimers());
    
      test('fake timeout with callback', () => {
        Samples.caculate1(1, 2, (result) => {
          expect(result).toBe(3);
        });
        jest.advanceTimersToNextTimer(); // 다음 예정된 타이머 실행
      });
    });
    
    • 위는 모두 동기적으도 동작한다. done() 함수를 호출할 필요가 없다.
    • advanceTimersToNextTimer() 함수를 반드시 실행한다. 아니면 가짜 타이머가 실행되지 않는다.

    일반적인 이벤트 처리하기

    이벤트 이미터(Event Emitter)

    • 액션이 완료되었음을 알리기 위해 메시지를 보내 이벤트를 발생시키는 객체
    • 해당 이벤트를 구독(subscribe)하고 이벤트 발생을 확인
    const EventEmitter = require('events');
    
    class Adder extends EventEmitter {
      constructor() {
        super();
      }
    
      add(a, b) {
        const result = a + b;
        this.emit('added', result);
        return result;
      }
    }
    
    module.exports = Adder;
    
    describe('events based module', () => {
      describe('add', () => {
        it('generates addition event when called', (done) => {
          const adder = new Adder();
          adder.on('added', (result) => {
            expect(result).toBe(3);
            done();
          });
          adder.add(1, 2);
        });
      });
    });
    
    • done() 함수를 사용함으로써 이벤트 발생 여부 확인 가능
    • expect(x).toBe(y)를 추가하면
      • 이벤트 매개변수로 전달된 값 확인 가능
      • 이벤트가 trigger되었는지도 확인 가능

    DOM 테스트 라이브러리

    • 화면상의 요소를 찾는 보일러 플레이트가 많았다.
    • 라이브러리를 사용하면 쉽게 해결 가능
    • 텍스트를 기반으로 탐색 쿼리 실행
    const { screen, fireEvent } = require('@testing-library/dom');
    
    const loadHtml = (fileRelativePath) => {
      const filePath = path.join(__dirname, fileRelativePath);
      const innerHTML = fs.readFileSync(filePath);
      document.documentElement.innerHTML = innerHTML;
      return document.documentElement;
    };
    
    const loadHtmlAndGetUIElements = () => {
      const docElem = loadHtml('index.html');
      const button = getByText(docElem, 'Click Me', { exact: true });
      return { window, docElem, button };
    }
    
    
    
    describe('index helper', () => {
      test("dom test lib button click triggers change in page", () => {
        const { window, docElem, button } = loadHtmlAndGetUIElements();
    
        // 이벤트 실행 간소화
        fireEvent.load(window);
        fireEvent.click(button);
    
        expect(findByText(docElem, 'Clicked', { exact: false }))toBeTruthy();
      })
    });
    
    • 페이지 요소를 찾기 위해 id/test-id를 별도로 지정하지 않아도 된다.
    • { exact: false } 옵션 : 대소문자 구분 안 함, 문자열 시작/끝에 누락된 문자 허용

    총평

    비동기 코드가 의존성급이라니

    처음에는 비동기 코드를 다루기 위해 진입점을 분리하기도 하고, 심지어 어댑터 방식으로 의존성처럼 취급하기도 하는 걸 보며 이렇게까지 해야 하나? 했는데, 비동기 함수에 대한 테스트로 인해 꽤나 애를 먹었던 기억이 있어서 이해가 됐습니다. 테스트에 맞춰 처리하는 것도 힘든데, 테스트를 짜는 건 얼마나 어려울까.

    하지만 잘 와닿지 않는 걸

    이번 장은 내용을 정리하기에 급급했습니다. 실제로 비동기 함수를 테스트할 때 참고하려는 용도로 기록을 남기고, 컨셉만 이해하는 방식으로 가볍게 읽었습니다.

    profile

    FE Developer 박승훈

    노력하는 자는 즐기는 자를 이길 수 없다