모든 개발자 채용의 자격 조건 및 우대 조건을 보면 이러한 키워드를 적어놓은 회사가 많다.
TDD란 Test Driven Development의 약자로 직역하자면 테스트 주도 개발이다. 간단하게 알아본 결과 코드를 작성하기 전에 테스트 코드를 작성하고 본 코드를 작성하는 개발 방법론 중 하나이다.
뭐 인터넷 검색하면 이런 이미지를 많이 볼 수 있는데, 먼저 테스트를 실패하는 코드를 적고 실제 코드를 작성한 후 코드를 리팩토링하는 방식이다. 일단 간단한 단위 테스트부터 공부를 하기 시작할 것이고 TDD에 대해선 Test가 뭔지 Jest를 어떻게 사용하는지를 먼저 배우고 천천히 더 알아보도록 하겠다.
이 글은 공부를 하며 기록한 글로 글에 오류가 있을 수 있습니다.
뭐 제대로 서비스 출시를 목적으로 둔 개발 경험이 없는 학생 개발자의 입장에선 어떻게 생각하면 괜한 일을 더 하는게 아닌가 하는 생각이 든다. 하지만 구글에 TDD, 단위 테스트 등을 키워드로 검색하면 테스트가 중요도를 설명하는 글이 많다. 최근에 Java의 객체지향과 Spring Boot를 공부하고 있는데 여기서 테스트 코드 작성의 필요성을 느낀적이 있다. 만약에 Spring Boot의 DaoService
, 즉 Dao라는 인터페이스를 implements
받아서 실제 데이터베이스와 연결하는 클래스가 있다고 하자.
@Repository("mysql")
이렇게 위에 Repository 어노테이션을 붙이고 mysql 데이터베이스를 연결하는 DaoService
가 있다고 하자. 이 클래스 내에는 들어가는 쿼리문 또는 JPA 메소드 등 데이터베이스와 연관된 트랜잭션 코드들이 존재한다.
그리고 Dao 인터페이스에는 Service와 매치되는 다양한 메소드가 있을 것이고 그 메소드에 @Qualifer("mysql")
을 통해 어떤 DaoService
와 연결할지 스프링 컨테이너에게 알려주게 된다.
만약 회사 개발 스택 변화로 MySQL을 버리고 PostgreSQL로 옮겨 간다고 해보자. 그러면 DaoService
를 다시 작성하게 될 것이고 데이터베이스의 인풋과 아웃풋은 이전과 완전히 동일해야한다. 회사의 데이터베이스는 정말 중요하기 때문에 이 결과가 동일한지 성능이 일정한지 체크해야 하는데 만약 이 경우 이전에 작성해놓은 데이터베이스 아웃풋 테스트 코드가 있다면 모두가 행복한 상황이 연출될 것이다.
요약하자면 대충 회사 DBMS를 바꿀 일이 생겼고 이로 인해 데이터베이스에 접근하는 코드를 변경했는데, 이전과 결과가 같은지 확인할 때 테스트 코드가 있으면 좋을 것 같다는 뜻
와! 내 나름대로 테스트 코드를 왜 작성하는지 이유를 찾게 되었다. 이제 간단하게 Jest를 공부하기 위한 개발 환경을 세팅해보겠다. 일단 기본적으로 NodeJS
가 설치되어 있어야 하고 패키지 매니저로는 yarn
을 사용하도록 하겠다.
프로젝트 폴더를 생성하고 yarn init
을 통해 package.json
폴더를 만들고
yarn add --dev jest ts-jest @types/jest typescript ts-node
위 패키지들을 설치해준다. jest에서 타입스크립트를 사용하려면 ts-jest를 설치해주어야 한다. 그러면 jest.config.js가 자동으로 생성되고 preset이 typescript
로 지정된다. 그리고
npx tsc --init
touch basic.test.ts
로 테스트를 할 타입스크립트 파일을 만들고 아래 처럼 작성한다.
interface CoffeeData {
id: number;
name: string;
price: number;
size: string;
};
const data:CoffeeData[] = [
{
id: 1,
name: "Americano",
price: 4100,
size: "Regular"
},
{
id: 2,
name: "Cafe Mocha",
price: 5600,
size: "Regular"
},
{
id: 4,
name: "Mango Juice",
price: 6300,
size: "Large"
}
]
test('testing test', () => {
expect(3).toBe(3);
})
test('coffe data has length of 3 ', () => {
expect(data).toHaveLength(3);
});
test('Checking Coffee\'s name', () => {
expect(data.map(coffee => coffee.name)).toEqual([
"Americano",
"Cafe Mocha",
"Mango Juice"
])
})
test('Testing Coffee Data Snapshot', () => {
expect(data).toMatchSnapshot();
})
먼저 data
는 간단하게 데이터베이스에서 넘어온 값들을 하드코딩해 보았고 아래는 기본적인 테스트 코드들이다.
test('testing test', () => {
expect(3).toBe(3);
})
이 코드는 testing test
라는 이름으로 콜백 함수 내의 테스트를 실행하게 한다. 메소드 이름이 굉장히 직관적인데 3이 3이길 기대한다
라는 뜻이다.
yarn jest
를 통해 테스트를 실행하면 콘솔에 테스트가 잘 되었다고 나온다.
test('coffe data has length of 3 ', () => {
expect(data).toHaveLength(3);
});
test('Checking Coffee\'s name', () => {
expect(data.map(coffee => coffee.name)).toEqual([
"Americano",
"Cafe Mocha",
"Mango Juice"
])
})
그 아래 테스트는 data가 3의 길이를 갖는지 확인하는 테스트 코드이고 그 아래는 data
를 map
배열 메소드를 통해 하나씩 이름을 확인해는 테스트이다. 전부 테스트를 잘 통과하고 만약 테스트 코드의 값이나 data
내의 값을 바꾸면 테스트가 실패하는 것을 콘솔에서 확인할 수 있다.
test('Testing Coffee Data Snapshot', () => {
expect(data).toMatchSnapshot();
})
그럼 그 아래 얘는 뭘까? 이 친구는 상황에 따라 두 가지 기능을 하는데, 하나는 최초로 실행시 그리고 남은 하나는 그 이후이다.
공식 문서를 보면 SnapShot 파트에 이렇게 적혀있다. 처음 실행하면 snapshot 파일을 생성한다.
라고 되어 있다.(이 후에 파일 명을 바꿔서 스냅샷 파일 이름이 다릅니다.)
실제로 처음 실행하면
이렇게 __snapshots__
폴더를 생성하게 되고 안에 현재 data
파일의 정보가 들어가게 된다. 다시 실행하면 data
파일과 snapshot 내의 값을 비교하여 테스트를 통과할지 말지 정해준다. 다시 data
에서
const data = [
{
id: 5, // 이 부분을 1에서 5로 바꿔보자
name: "Americano",
price: 4100,
size: "Regular"
},
{
id: 2,
name: "Cafe Mocha",
price: 5600,
size: "Regular"
},
{
id: 4,
name: "Mango Juice",
price: 6300,
size: "Large"
}
]
데이터 하나의 id
를 살짝 5로 바꾸고 다시 테스트를 돌리면 snapshot 테스트는 실패하는 것을 확인할 수 있다.
이렇게 자세히 어느 부분이 틀렸는지도 보여준다. 코드의 사이즈가 정말 크다면 정말 유용하게 사용 될것 같다.
이외에도 expect().
이후에 유용한 메소드가 많기 때문에 상황에 따라 다양한 테스트를 해볼 수 있다. 공식문서를 확인하면 regex, array 심지어는 exception까지 다양한 형태의 테스트가 가능함을 확인할 수 있다.
Jest에서는 다양한 비동기 함수도 테스트할 수 있다. 생각했던 것과 많이 다르진 않고 직관적이다.
const fetchData = (coffee) => {
return new Promise((resolve, reject) => {
// return resolve(coffee);
return reject('ERROR!');
});
}
test('promise resolves', () => {
const data = 'americano';
return expect(fetchData(data)).resolves.toBe(data);
})
test('promise rejects', () => {
const data = 'americano';
return expect(fetchData(data)).rejects.toMatch('ERROR!');
});
위와 같이 resolve하는 경우와 reject하는 경우를 나누어서 테스트할 수 있다. async 함수도 아래와 같이 작성해보자.
test('async await test', async () => {
const data = 'americano';
const value = await fetchData(data);
expect(value).toMatch(data);
})
이렇게 test의 콜백 함수에 async를 붙여줌으로서 비동기 함수로 부터 받은 리턴 값도 테스트가 가능하다.
이번엔 Jest의 기본적인 테스트 방식에 대해서 알아보았다. 다음엔 함수와 함수의 반환을 테스트하는 mock에 대해서 알아보도록 하겠다.