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 의 명령어인 expect와 toBe 를 사용하여 테스트를 진행해준다.
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 에서 제공하는 리액트 테스팅 도구중 render 와 screen 을 사용한다.
- 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',
});
});
'학원에서 배운 것' 카테고리의 다른 글
[포스코x코딩온] 주차 회고 - 2023.02.22 ~ 팀프로젝트 시작 (0) | 2023.02.24 |
---|---|
[ 포스코 x 코딩온 ] 주차 회고 및 프로그래머스 옹알이 풀이 (0) | 2023.02.17 |