RESTful API 서버 테스트하기
요즘 소프트웨어 개발자라면 자동화 된 테스트가 필요하다는 것에 대부분 동의할 것입니다. 그러나 테스트를 짜본 적이 없으면 처음엔 어떻게 해야 할지 막막하기만 합니다. 저도 그랬고, 공부하려 해도 라이브러리 사용법이나 뜬구름 잡는 소리(TDD? BDD?)는 많은데 구체적인 방법은 찾아보기 힘든 것 같습니다. 그래서 이 글에서는 RESTful API 서버로 영역을 한정하여 하나의 테스트 작성 방식을 제시해보려 합니다.
애플리케이션 아키텍쳐에 대한 가정
특정 프레임워크에 한정되는 내용 은 아니므로 일반적인 MVC 아키텍쳐를 가정하고 설명할 것입니다.
- 모델: 데이터베이스에 접근하는 모듈. 서비스/DAO처럼 추상화 되어 있는 경우도 모델로 통칭하기로 합니다.
- 컨트롤러: HTTP 요청을 받아 다듬은 다음 모델 계층을 호출해서 요청을 수행하고 결과를 돌려주는 모듈. 뷰, 요청 핸들러, 리소스라고 부르기도 합니다.
- UI 테스트는 다른 접근 방식이 필요하기 때문에 뷰/템플릿은 고려하지 않고 JSON만을 돌려준다고 가정합니다.
- 단일(monolithic) 애플리케이션을 가정합니다. 여러 작은 애플리케이션끼리 통신하는 아키텍쳐(SOA/마이크로서비스)에서는 다른 전략이 필요할 것입니다.
외부 인터페이스를 통해 통합 테스트
테스트에 대한 자료를 검색해보면 단위 테스트에 대한 것이 대부분인 것 같습니다. 단위 테스트는 하나의 모듈(클래스, 함수)을 격리시켜서 테스트하는 방법입니다. 한편 통합 테스트는 여러 모듈을 결합하여 시스템이 전체적으로 잘 동작하는지 테스트하는 방법이라고 할 수 있습니다.
비교적 외부에 노출되는 인터페이스가 명확한 RESTful API는 모듈을 각각 테스트하지 않아도 가장 상위 모듈을 테스트하면 하위 모듈도 대부분 커버됩니다. 구체적으로는 컨트롤러를 테스트하면 모델 계층도 거의 테스트됩니다. 따라서 통합 테스트만 작성하는 것을 기본으로 하고, 단위 테스트는 단위 테스트 방식으로 접근하면 노력이 덜 드는 경우에만 작성하는 것이 효율적입니다.
컨트롤러를 사용해서 테스트하는 방법은 컨트롤러 객체/함수를 직접 호출해서 테스트하는 방법과 HTTP 클라이언트를 사용하는 방법이 있습니다. 사용자는 HTTP를 통해 API에 접근할 것이고, 또한 URL 라우팅이나 미들웨어까지 포함해 테스트하기 위해서는 HTTP 클라이언트를 사용하는 것이 좋습니다. (물론 꼭 서버를 띄워서 소켓 통신을 해야 한다는 뜻은 아닙니다. 많은 프레임워크가 HTTP 요청을 시뮬레이션하는 방법을 제공하고 있으므로 그런 기능을 사용하면 됩니다.)
실제 데이터베이스를 사용
테스트 시 데이터베이스를 끼워넣는 전략이 여러가지 있습니다. 먼저 아예 데이터베이스를 사용하지 않는 방법입니다. 데이터베이스에 접근하는 인터페이스를 추상화하고 가짜 객체를 만듭니다. 이렇게 하면 데이터베이스의 동작을 매번 코드로 작성해줘야 하는 불편함이 있고 실제 환경과 거리가 멀어지므로 통합 테스트에는 적합하지 않습니다. 또한 SQL 기반의 RDBMS를 사용한다면 DB에도 로직을 어느 정도 맡기게 되므로 DB까지 포함해서 테스트하는 것이 좋다고 생각합니다.
실제 데이터베이스를 사용하더라도, ORM을 사용한다면 서비스에 쓰는 DB보다 가벼운 SQLite나 HSQLDB 같은 인메모리 DB를 사용해서 테스트할 수 있습니다. 하지만 ORM의 추상화가 항상 완벽하지는 않으므로 서비스에서 쓰는 것과 같은 종류의 DB를 띄워서 테스트하는 것이 좋습니다.
많은 프레임워크들이 테스트 시 DB를 비우는 기능을 제공하고 있기 때문에 개발 환경에 DB를 설치하는 부담을 제외하면 크게 불편함은 없는 것 같습니다. 요즘은 Docker 같은 기술도 있으니 로컬 개발 환경에 DB를 쉽게 설치할 수 있을 것입니다.
물론, 로컬 환경에 띄울 수 없는 외부 서비스는 추상 인터페이스를 만들고 가짜 객체를 사용하는 수 밖에 없겠습니다.
테스트를 작성할 예제 API
간단한 할일 목록 API를 테스트한다고 생각하겠습니다. 다음과 같은 API 엔드포인트가 있다고 합시다.
GET /tasks
- 저장된 할일 목록을 최근에 추가된 것 먼저 돌려줍니다.
excludeCompleted=true
파라미터를 지정하면 완료되지 않은 목록을 돌려줍니다.
POST /tasks
- 할일을 추가합니다.
- 추가된 할일 객체를 돌려줍니다.
Task 객체는 다음과 같이 생겼습니다.
{
"id": "idididid",
"text": "Write tests",
"completed": false
}