본문 바로가기
학원에서 배운 것

QA 라이브러리, JEST!

by 쿠리의일상 2023. 5. 5.

Jest

QA는 과거 대비 고객 만족도가 중요해진 현대에 각광 받고 있는 분야로, 모든 서비스에서 QA를 부서를 따로 둘 수 없기 때문에 개발자가 직접 테스트를 하게 된다.

 

예전에는 모든 테스트를 하나로 수행할 순 없이 Mocha 나 Jasmin으로 실행했지만 실제로 값을 비교하는 것은

Chai, Expect 를 사용하고 테스트 케이스는 Sinon, Testdouble을 썼었다고 한다.

대체로 유사한 방식이지만 다른 부분이 있어서 어려움이 존재했는데,

위의 여러가지를 하나에서 전부 수행할 수 있도록 해준 라이브러리가 JEST라고 한다.

 

 

Jest 세팅

npm i -D jest

테스트는 jest를 이용할 것이므로 package.json 파일에서 스크립트 test 를 jest 로 수정해준다.

 

이제 npm test 명령어를 통하여 파일이 xxx.test.js 와 같이 되어 있거나 __test__ 폴더 내부의 테스트 파일 전부를 테스트해준다.

 

 

테스트 해보기

테스트용 함수 

//jest 테스팅 용
function sum(a, b) {
  return a + b;
}

module.exports = sum;

 

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

test('테스팅', () => {
  expect(sum(1, 2)).toBe(3);
});

테스트할 함수를 테스트할 실제 Suite 를 코드로 작성

jest 의 명령어인 expecttoBe 를 사용하여 테스트를 진행해준다.

 

test('테스트를 설명할 문구', () => {
	expect(검증대상).toXX(기대결과);
});

1. 검증대상 : 검증하고자하는 함수나 api 등이 포함

2. toXX( = Matcher) : 검증하고자 하는 방식에 따라 메서드가 달라짐

 

 

Matcher

toBe / toEqual

toBe : 얕은 비교 (값만)

const makeObj = (id, name) => {
	return { id, name };
};

test('toBe, toEqaul 비교', () => {
	expect(makeObj('test', '테스트')).toBe({ id: 'test', name: '테스트' });
});

toEqual : 깊은 비교 (값과 타입까지)

const makeObj = (id, name) => {
  return { id, name };
};

test('toBe, toEqaul 비교', () => {
  expect(makeObj('test', '테스트')).toEqual({ id: 'test', name: '테스트' });
});

 

  • js 에서 객체의 얕은 비교는 객체의 주소를 비교하고 깊은 비교는 객체 내부의 key 와 값을 비교한다.
    • toBe 방식으로 함수에서 생성된 객체와 실제로 주어진 객체의 서로 주소가 다르므로 서로 다르다는 결과를 나타내게 된다.
    • toEqual 은 객체의 내부의 key 와 값을 전부 비교하여 해당 객체의 데이터가 동일한지 비교한다.

 

toStrictEqual

toEqual 의 경우 undefined 로 지정된 값은 테스팅 하지 못한다. 이럴 때 사용하는 Matcher가 toStrictEqual 이다.

비교할 땐 toEqual 보단 toStrictEqual 을 사용하는 것이 좋다.

 

toHaveLength

특정 배열의 길이를 테스트하는 Matcher

export interface User {
  name: string,
  email: string,
  age: number,
}

const users: User[] = [
  { name: 'XXX', email: '123@333', age: 30},
  { name: 'zzz', email: '222@333', age: 31},
  { name: 'VV', email: '555@333', age: 25},
  { name: 'YYYY', email: '444@333', age: 28},
];

const getAllUsers = ():User[] => {
  return users;
};

test('길이확인!', () => {
  expect(getAllUsers()).toHaveLength(4);
});

 

toContainEqual

특정 배열 값에서 원하는 배열이 존재하는지 확인하는 Matcher

객체가 아닌 원시값을 테스트 할 땐 toContain으로도 OK

test('회원 존재 여부 확인', () => {
  expect(getAllUsers()).toContainEqual({ name: 'YYYY', email: '444@333', age: 28});
});

 

toBeGraterThan / toBeGreaterThanOrEqual

toBeLessThan / toBeLessThanOrEqual

숫자 비교를 위한 Matcher

const getCountsOverAge = (age: number): number => {
  const resultArr: User[] = users.filter((el :User) => el.age >= age);
  return resultArr.length;
}

test('30살 이상인 회원 테스트', () => {
  expect(getCountsOverAge(30)).toBeGreaterThanOrEqual(2);
})

 

 

toMatch

정규식을 이용하여 특정 문자열의 포함 여부 또는 문자열 검사를 활용할 수 있는데,

해당 기능을 사용하여 특정 이름을 가진 회원의 email이 email 형식을 지키고 있는지 테스팅 할 수 있다.

const getEmailToName = (name: string): string => {
  const result: User | undefined = users.find((el :User) => el.name === name);
  if(result !== undefined)
    return result?.email;
  
  return '회원 없음';
}

test('이메일 형식 확인', () => {
  expect(getEmailToName('zzz')).toMatch(
    /^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/
  );
})

 

toThrow

예상된 에러를 발생시키거나 예상하지 못한 에러가 발생할 때 테스팅용 Matcher

const throwError = ():never => {
  throw new Error('에러');
}

test('에러 발생!', () => {
  expect(() => throwError()).toThrow();
})
///// toThrow 로 테스팅할 땐 Error 를 발생시키는 함수를 익명 함수로 한번 감싸줘야 Error를 발생시키는 함수에서 발생한 에러가 그대로  출력되지 않는다.

 

 


TS 와 Jest

Babel 을 사용하는 방식은 TS 를 JS 로 변환하고 그것을 다시 jest 로 테스트하는 방식은 효율이 좋지 않으므로

ts-jest 를 이용하여 타입스크립트를 테스팅할 수 있다.

npm i -D ts-jest @types/jest

 

ts-jest 세팅하기

npx ts-jest config:init

위 명령어를 실행하면 jest.config.js 파일이 생성된다.

 

 

 

비동기 테스트

Callback, Promise, Async-Await

 

Callback

export const getNameCB = (callback: (str: string) => void): void => {
  const name :string = 'Kim';

  setTimeout(() => {
    callback(name);
  }, 2000);
};

test('2초 뒤 이름을 받아오는 콜백 함수 테스팅', () => {
  function callback(name: string) :void {
    expect(name).toBe('Kim');
  }

  getNameCB(callback);
})

위 코드는 2초 뒤에 실행되어야 하는데 1초만에 처리가 되어버리는 문제가 발생한다.

js 가 테스트 함수에서 콜백함수를 기다려주지 않기에 일단은 통과를 시켰다가 나중에 에러가 발생하게 된다고 한다.

 

이럴 땐 done 이라는 콜백함수를 전달하고 사용하여 콜백함수를 기다려주도록 처리해줘야 한다.

test('2초 뒤 이름을 받아오는 콜백 함수 테스팅', (done) => {
  function callback(name: string) :void {
    expect(name).toBe('Kim');
    done();
  }

  getNameCB(callback);
})

 

콜백에서 Error 받기

export const getNameCBError = (callback: (data: any) => void): void => {
  const name :string = 'Kim';

  //50% 확률로 에러를 발생시켜 에러를 콜백함수에 담아서 전달
  setTimeout(() => {
      try {
        if(Math.floor(Math.random() * 2) % 2 === 0) {
          callback(name);
        } else {
          throw new Error('에러');
        }
      } catch(err) {
        callback(err);
      }
  }, 2000);
};

test('2초 후에 이름을 받아오는 콜백 함수 테스팅', (done) => {
  function callback(data: any): void {
    try {
      if(data instanceof Error) {
        expect(data).toBe('에러');
      } else {
        expect(data).toBe('Kim');
      }
      done();
    } catch (err) {
      done(err);
    }
  }

  getNameCBError(callback);
})

 

 

Promise

const getNamePromise = (): Promise<string> => {
  const name = 'Kim';
  return new Promise<string>((res, rej) => {
    setTimeout(()=>{
      res(name);
    }, 2000);
  });
}

test('2초 후에 이름을 받아오는 프로미스 함수 테스팅', () => {
  return getNamePromise().then((age: string) => {
    expect(age).toBe('Kim');
  })
})

콜백함수에서 done 으로 받아주던 것을 프로미스에선 return 처리를 해줘야 정상 처리된다.

resolve 라는 프로미스 이행 함수가 return 되어야 Pending 이 풀리고, return 이 안되면 그냥 종료가 되기 때문이라고 한다.

 

Promise 에서의 Error

reject 상황을 catch 로 받아주면 된다.

const getNamePromise = (): Promise<string> => {
  const name = 'Kim';
  return new Promise<string>((res, rej) => {
    setTimeout(()=>{
      if(Math.floor(Math.random() * 2) % 2 === 0) {
        res(name);
      } else {
        rej(new Error('에러'));
      }
    }, 2000);
  });
}

test('2초 후에 이름을 받아오는 프로미스 함수 테스팅', () => {
  return getNamePromise()
    .then((age: string) => {
      expect(age).toBe('Kim');
    })
    .catch((err) => {
      expect(err.message).toBe('에러');
    })
})

try/catch 대신 간단하게 성공과 에러 상황만 비교하고 싶다면

resolves, rejects 를 사용

test('2초 후에 이름을 받아오는 프로미스 함수 테스팅', () => {
  return expect(getNamePromise()).resolves.toBe('Kim');
})

////
test('2초 후에 이름을 받아오는 프로미스 함수 테스팅', () => {
  return expect(getNamePromise()).rejects.toBe('에러');
})

 

 

Async/Await

const getNameAsync = (id: string): Promise<string> => {
  const name = 'Kim';

  return new Promise<string>((res, rej) => {
    setTimeout(() => {
      if(id === 'Kim') {
        res(name);
      } else {
        rej(new Error('에러'));
      }
    }, 2000);
  });
}

test('2초 후에 이름을 받아오는 async 함수 테스팅' , async () => {
  try {
    const res: any = await getNameAsync('Kim');
    expect(res).toBe('Kim');
  } catch (err) {
    expect(err.message).toBe('에러');
  }
});

 

 

리액트와 jest

// App.test.js

import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

기본적으로 CRA 으로 만들어진 리액트는 jest 를 사용한 테스팅을 지원해준다.

 

jest 에서 제공하는 리액트 테스팅 도구중 renderscreen 을 사용한다.

  • render 는 해당 컴포넌트를 그려주고
  • 그려준 컴포넌트는 screen에서 검증할 수 있음 

 

위의 기본 test 코드는

-> APP 컴포넌트와 해당 html 코드를 분석

-> html 코드 내부에서 learn react 코드가 있다면 테스트를 통과할 수 있다는 의미임

 

 

리액트 조건에 따라 테스팅 해보기

import React from 'react'

export default function HeaderTesting({
  isLogin,
  userID,
} : {
  isLogin: boolean,
  userID: string,
}) {
  return (
    <div>
      {isLogin ? (
        <h1>{userID} 님 환영합니다.</h1>
      ) : (
        <h1>
          로그인을 해주세요. <button>로그인</button>
        </h1>
      )}
    </div>
  );
}

기본 헤더에 로그인 여부에 따라 테스팅 하는 jest 코드 작성

 

// App.test.js

import { render, screen } from '@testing-library/react';
import HeaderTesting from './pages/HeaderTesting';

test('isLogin 이 true 인 케이스에서 로그인 문구 변경 테스팅', () => {
  render(<HeaderTesting isLogin={true} userID="Kim" />);
  const txtElement = screen.getByText(/Kim 님 환영합니다./i);
  expect(txtElement).toBeInTheDocument();
});

getByText() 는 해당 문구가 그려진 요소를 찾는 메서드

toBeInTheDocument() 메서드는 Document 에 존재하는지 여부를 확인

 

// App.test.js

test('isLogin 이 false 인 케이스에서 로그인 문구 변경 테스팅', () => {
  render(<HeaderTesting isLogin={false} />);
  const txtElement = screen.getByText(/로그인을 해주세요./i);
  const btnElemnet = screen.getByRole('button');
  expect(txtElement).toBeInTheDocument();
  expect(btnElemnet).toBeInTheDocument();
  expect(btnElemnet).toHaveTextContent('로그인');
});

특정 역할을 하는 요소를 가져오는 getByRole() 메서드와

특정 요소의 내부 컨텐츠를 활용하는 toHaveTextContent() 메서드 활용

 

 

import React from 'react'

export default function JoinBtn({ age } :{ age: number}) {
  return (
    <>
    {age > 14 ? <label>회원 가입이 가능</label> : <label>회원 가입이 불가능</label>}
    <button disabled={age > 14} style={
      age > 14 ? {backgroundColor : 'blue'} : {backgroundColor : 'red'}
    }>가입</button>
    </>
  )
}


test('20세일 때', () => {
  render(<JoinBtn age={20} />);

  const textEl = screen.getByText(/회원 가입이 가능/i);
  const btnEl = screen.getByRole('button');

  expect(textEl).toBeInTheDocument();
  expect(btnEl).toBeInTheDocument();
  expect(btnEl).toBeDisabled();
  expect(btnEl).toHaveStyle({
    backgroundColor: 'blue',
  });
});

test('13세일 때', () => {
  render(<JoinBtn age={13} />);

  const textEl = screen.getByText(/회원 가입이 불가능/i);
  const btnEl = screen.getByRole('button');

  expect(textEl).toBeInTheDocument();
  expect(btnEl).toBeInTheDocument();
  expect(btnEl).toBeEnabled();
  expect(btnEl).toHaveStyle({
    backgroundColor: 'red',
  });
});