본문 바로가기
개발

MSSQL SP(Stored Procedure) 테스트 환경 구축하기

by 상5c 2022. 7. 10.

테스트란?

테스트(test)란? : 위키백과

시험(試驗): 지식, 기술, 능력 따위를 평가하고 검사하는 일
실험(實驗): 가설이나 이론이 실제로 들어맞는지를 확인하기 위해 다양한 조건 아래에서 여러가지 측정을 실시하는 일

테스트 코드

코드가 실제로 원하는 동작을 수행하는지 측정을 실시하는 코드

테스트 방법

사람이 하기
  1. 원하는 값을 DB에서 찾아서..
  2. 서
비스(앱 또는 웹)에서 특정 동작 수행
  3.
바뀐 결과 확인 (DB에 잘 저장됐나?)
  4. P
OSTMAN으로 API 호출…
  5.
호출 결과 확인.. (응답이 잘 왔나? DB값이 원하는대로 바꼈나?)
  6.
에러났으면 다시 데이터, 코드 수정
  7.
반복

코드로 하기
  1. 원하는 값 세팅(코드 또는 SQL 파일)
  2.
테스트 코드 작성
  3.
실행
  4.
결과 보기
결과 검증도 코드가 해준다

SP는 어떻게 테스트하지? 테스트 코드는 코드 동작 과정, 결과를 검증하기 위해 작성한다.
근데 SP는 코드가 아닌데???

블랙박스 테스트를 해보자! (input/output만 관심)

테스트 격리

테스트는 격리되어야 한다.

  • 테스트 결과가 실제 환경에 영향을 미쳐선 안된다.
  • 외부 요인(누군가의 DB 수정 등)이 테스트 결과에 영향을 미쳐선 안된다.
  • 테스트는 동일한 input에 대해 동일한 output을 제공한다. (멱등성)

위 조건들을 만족하기 위해 테스트가 동작하는 환경 분리가 필요함.

테스트 환경 구축

인프라 격리 - 테스트 전용 Database가 필요하다.

테스트를 성공시키기 위해 고려했던 여러가지 방법들

  • Repository mocking
    • DB(SP)에 대부분의 로직이 담겨있어 DB 연동 없는 테스트는 의미가 없음.
  • Embedded Database
    • 일반적으로 선택하는 방법. Test code 동작시 in memory db가 실제 db를 대신한다.
    • JPA를 사용한다면 RDB는 H2 Database를 사용
      • MSSQL은 in memory 형태를 제공하지 않음
    • 장점
      • 별도의 설정이 필요하지 않음.
    • 단점
      • H2 Database가 MSSQL과 동일한 동작을 수행할 것이라고 보장하지 않음.
        • Create SP가 가능한지 모름 (시도해보지 않았음)
      • SP를 실행해야 하기에 문제가 있음.
  • TestContainers
    • 테스트코드 동작시에 Docker를 사용해 Container를 띄우고, 실제 database를 대신함.
      • 요즘 많이 사용하는 것으로 보임.
      • 실제 운영 환경과 동일한 테스트를 위해 사용됨
      • 작년?에 당근 밋업가서 들었음
        • 컨테이너 뜨는데 오래걸리니까 로컬에서는 도커를 미리 띄워놓고 하고 빌드서버에서 TestContainers 동작하는 식으로 한다고 함.
    • 인프런 백기선님 강의에도 기본적인 사용법 관련된 내용이 있음
    • 장점
      • docker-compose 사용 가능 → 운영 환경과 동일한 DB 구성 가능
      • 실제 환경과 동일한 동작 결과
    • 단점
      • 도커 컨테이너가 실행되고 나서 테스트가 동작하기 때문에 임베디드 db보다 오래걸림

테스트는 어떻게 하지?

NEXTSTEP 강의에서 배운 인수테스트를 사용해보자.

  • 인수테스트?
    • 명세나 계약의 요구사항이 충족되는지 확인하기 위해 수행되는 테스트
    • 블랙박스 테스트의 성격을 갖는다.
      • 내부 구현을 바꿔도 테스트가 영향받지 않음
      • 내부 동작에 관심이 없기 때문에 SP를 사용하는게 문제가 되지 않음!
    • request, response에만 관심
      • 블랙박스 테스트. 호출이 성공적으로 이루어졌고 결과를 제대로 받았는지가 중요함.

Rest Assured 사용

  • 실제 환경과 가장 비슷하게 구성 가능. 임베디드 톰캣이 로드된다.
  • 다른 테스트 클라이언트
    • MockMvc
    • WebTestClient
      • 톰캣이 아닌 네티 사용

사용 예

public static ExtractableResponse<Response> 결제_요청(Map<String, String> cookies) {

  return given().log().all()
        .accept(MediaType.APPLICATION_JSON_VALUE)
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .cookies(cookies)

        .when()
        .post("/api/v1/xxx")

        .then().log().all()
        .extract();
}
  • Rest assured는 HTTP를 통해 호출하기 때문에 CRUD에 대해 API를 갖지 않는 경우 (외부 의존성을 갖는 경우) 수동으로 SQL문을 작성해서 데이터를 세팅하고 테스트하는 과정이 필요함
  • 약간의 고통
    • 테스트하고자 하는 기능의 CRUD를 모두 갖고있지 않다.
      • 일부 SQL문을 직접 실행하는 형태로 대체해야 한다.
    • DB 스키마를 직접 구성해야 한다.
      • JPA는 테이블을 만들어줌. (CREATE를 안해도 되는게 아니라 대신 해준다)
    • SP를 수동으로 업데이트 해야 한다.
      • 로직이 SP에 들어있다. SP는 DB에 들어있다.
      • DB코드로 테스트한다. layer가 달라 수동으로 업데이트 해줘야 한다.

TestContainers를 통해 테스트를 수행하는 과정

  1. docker compose up
  2. 테스트 코드 동작
  3. database cleanup
  4. 모든 테스트가 종료될때까지 2, 3 반복
  5. docker compose down

막혔던 부분

  • 라이센스 동의를 해야하는데 TC를 사용하면 공홈에서 제공하는 샘플코드로 동의처리가 안된다.
    • 앰버서더 컨테이너를 통하지 않고 직접 연결
  • SQL Server는 initdb 기능이 없다. 스키마와 초기 데이터 세팅을 직접 해줘야한다.
    • 첫 번째 시도. 코드로 sql 파일 불러와서 schema, data init
@BeforeAll
static void beforeAll() {
    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScripts(
            new ClassPathResource("test-schema.sql"),
            new ClassPathResource("test-data.sql"));
    populator.setSeparator("@@");
    populator.execute(this.dataSource);
    // run code that uses the test schema and data
}

 

  • Spring reference
  • 문제점
    • MSSQL은 GO를 입력해야 실행됨. 자동생성된 스크립트에 GO 구문이 포함되어 있음.
    • 근데 코드로 하면 JDBC를 통한 실행이라 MS-SQL의 “GO”같은 구문 해석이 안됐음. (separator를 이용하면 될지도?)
    • 계속 수동으로 SP 생성문을 업데이트 해줘야 했기에 DB 클라이언트를 통해 자동 생성된 CREATE TABLE, SP 구문을 그대로 복사 붙여넣기하여 사용할 수 있어야 했음.
  • 해결 방안
    • 스키마는 컨테이너 실행시, 데이터는 테스트 실행시 넣어주는 형태로 분리.

mssql Dockerfile

FROM mcr.microsoft.com/mssql/server:2017-latest

USER root

RUN echo "hello world!"

COPY ./ /setup/
RUN chmod +x /setup/import-data.sh
RUN chmod +x /setup/execute-sqls.sh

# A & B -> 앞의 명령어(A) 백그라운드로 실행하고 B 실행
# mssql 서버가 실행되어야 SQL을 실행할 수 있기 때문에 이런 형태가 됨
ENTRYPOINT /setup/import-data.sh & /opt/mssql/bin/sqlservr

import-data.sh

#!/bin/bash

# mssql 로드를 기다리기 위한 반복문
for i in {1..10}; do
  /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Pass@word -d master -i /setup/init.sql

  if [ $? -eq 0 ]; then
    echo "database setup start"
    /setup/execute-sqls.sh
    echo "setup completed"
    break
  else
    echo "not ready yet... [retry: $i]"
    sleep 1
  fi
done

 덕분에 schema 초기화는 완료

테스트 코드에서 data init

    • @Sql사용하여 SQL 스크립트 실행

 

  • 클래스 레벨에 사용 → @BeforeEach와 같은 역할
  • 메소드 레벨에 사용 → 해당 메소드만 적용
  • 둘 다 사용 → 메소드 레벨에 사용한 Sql로 Override된다.
    • @SqlMergeMode 어노테이션으로 둘 다 실행 가능

예상(원하는) 작업 순서

1. DB Start(container start)
2. Schema init
3. Data init
4. 테스트 코드 동작

실제 동작한 순서

1. DB Start
2. (동시) Schema init + Data init
3. 테스트 코드 동작 (실패)

schema 초기화가 완료된 상태로 DB가 실행되고, 이후에 테스트 코드가 실행되어야 했으나 테스트 코드는 data init이 완료된 시점을 알 수 없다. DB 커넥션이 맺어지면 실행됨.

근데 MSSQL docker는 initdb 지원 안함…

 

mssql-docker github issue

initdb 예) MySQL initdb

MySQL docker hub

 

 

 

세 번째 시도. Docker multi-stage build 적용으로 해결

이미지 생성 과정을 두 개의 스테이지로 나눈다.

- 첫 스테이지에서 데이터를 저장하고 종료
- 두 번째 스테이지에서 데이터를 불러와서 이미지 빌드.

이 방법을 사용하면 스키마 초기화가 완료된 채로 컨테이너 실행이 가능!

참고: docker multi-stage build

FROM mcr.microsoft.com/mssql/server:2017-latest AS builder

USER root

ENV ACCEPT_EULA=Y
ENV SA_PASSWORD=Pass@word

COPY ./ /setup/
RUN chmod +x /setup/import-data.sh
RUN chmod +x /setup/execute-sqls.sh

# import-data 완료시 마지막에 db shutdown을 수행 (sqlserver가 떠있으면 빌드가 끝나지 않음)
RUN /setup/import-data.sh & /opt/mssql/bin/sqlservr; exit 0


FROM mcr.microsoft.com/mssql/server:2017-latest

COPY --from=builder /var/opt/mssql/ /var/opt/mssql/

 

하나의 테스트가 다른 테스트에 영향을 주어선 안된다. 따라서 매 테스트마다 schema는 남기고 data만 날리는 truncate가 필요함

깨끗한 테스트를 위해 @BeforeEach 를 통해 데이터를 비우는 작업을 진행한다.

주의: @Sql이 먼저 실행된 후 @BeforeEach가 실행되어 세팅한 데이터가 지워진다.

@Sql 어노테이션을 사용하는 경우는 @AfterEach로 옮겨서 작업해야 한다.

 

일부 코드 mocking 불가능한 형태. 의존성 주입받는 형태로 변경이 필요했음.

new 대신 autowired

구현체 대신 interface 의존

 

기타 참고자료