반응형

https://sttp.site/

위 주소의 온라인 북 "소프트웨어 테스팅: 이론부터 실무까지(Software Testing: From Theory to Practice)"에서 섹션 4.2 SQL Testing의 내용을 발췌함

 


 

통합 테스트에서 흔히 마주하는 것이 데이터베이스와 통신하는 클래스이다. 비즈니스 애플리케이션이 복잡한 SQL 쿼리를 수행하는 다수의 DAO(Data Access Objects)로 구성되는 경우가 흔하다. 이러한 쿼리에 많은 비즈니스 지식이 포함되어 있으므로 이게 예상 결과를 생성하는지 확인하는 데 테스터가 노력을 쏟을 필요가 있다.

 

SQL 쿼리에서 무엇을 테스트해야 하는가?

SQL은 강력한 언어이며 개발자가 사용할 수 있는 다양한 기능/함수를 포함하고 있다. 단순화하면 쿼리가 프레디킷(‘또는 거짓으로 평가되는 표현식)으로 구성되어 있다고 생각할 수도 있는데, 아래 SQL 쿼리 예에서 빨간줄로 표시된 부분이 프레디킷이다.

 

테스터는 여러 다른 프레디킷을 실행시키고, 그것이 다른 결과(‘또는 거짓’)로 평가될 때 SQL 쿼리가 예상 결과(expected result)를 반환하는지 확인하는 것을 하나의 기준으로 삼을 수 있다. 그리고 여기에 테스팅 교재에서 논의되는 거의 모든 테스팅 기법을 적용할 수 있다.

  • 명세 기반 테스트(Specification-based testing): SQL 쿼리는 요구사항에서 생겨난다. 테스터가 요구사항을 분석하고 테스트해야 하는 동등 분할(equivalent partitions)을 도출할 수 있다.
  • 경계 분석(Boundary analysis): 이러한 프로그램에는 경계가 존재한다. 경계가 버그 가능성이 높은 장소라는 것을 예상할 수 있으므로, 이 부분을 실행하는 것이 중요하다.
  • 구조적 테스트(Structural testing): 구조적으로 SQL 쿼리가 프레디킷을 포함하고 있으며, 테스터가 이 SQL의 구조를 테스트케이스 도출에 사용할 수 있다.

 

이 중 구조적 테스팅에 초점을 맞추고, 위의 세 번째 쿼리 예에 구조적 테스팅 원리를 적용해 보자. SQL 쿼리가 두 개의 프레디킷으로 구성된 단일 브랜치(value > 50 and value < 200)를 가지는 것을 알 수 있다. 주어진 두 개의 프레디킷에는 (TT), (TF), (FT), (FF)의 네 가지 가능한 결과 조합이 존재하며, 테스터가 다음을 목표로 할 수 있다.

  • 브랜치 커버리지(Branch coverage): 두 개의 테스트(하나는 전체 디시젼을 ‘참’으로 평가되도록 하고 다른 하나는 전체 디시젼을 ‘거짓’으로 평가되도록 만듬)로 100% 브랜치 커버리지를 달성할 수 있다.
  • 조건+분기 커버리지(Condition+Branch coverage): 이 경우 100% 조건+분기 커버리지 달성을 위해 세 개의 테스트가 필요하다(예, T1=150, T2=40, T3=250).

 

관련 논문으로 “A practical guide to SQL white-box testing”에서는 구조적 테스팅 커버리지(구체적으로는 MC/DC 커버리지)를 적용하여 SQL 테스트 설계를 하는 가이드라인을 제안한다(상세한 내용을 보려면 클릭).

 

 

SQL 쿼리를 위한 자동 테스트케이스 작성 예

SQL 테스트 작성을 위해 JUnit을 사용할 수 있는데, 테스트가 (1) 데이터베이스와의 연결을 설정하고, (2) 데이터베이스가 올바른 초기 상태에 있는지 확인하고, (3) SQL 쿼리를 실행하고, (4) 출력을 확인하는 것으로 구성된다.

 

예를 들어, name(varchar, length 100)value(double)로 구성된 Invoice 테이블이 있고, 데이터베이스와 통신하기 위해 API를 사용하는 InvoiceDao 클래스가 있다고 가정하자(정확히 어떤 API가 사용되는지는 중요하지 않음). DAO가 아래 세 가지 액션을 수행한다.

  • save(): 데이터베이스에 송장(invoice)을 저장. SQL 쿼리 insert into invoice (name, value) values (?,?)를 실행한다.
  • all(): 데이터베이스에 있는 모든 송장을 반환. SQL 쿼리 select * from invoice를 실행한다.
  • allWithAtLeast(): 지정된 값 이상의 모든 송장을 반환. SQL 쿼리 select * from invoice where value >= ?를 실행한다.

 

이 예의 JUnit 테스트 코드 일부가 아래와 같다.

  • 각 테스트 전에 클린업(정리) 오퍼레이션을 수행한다. 데이터베이스에 알 수 없는 데이터(unknown data)가 있는 경우 SQL 쿼리가 예기치 않은 결과를 반환할 수 있으므로, 우리 테스트가 불안정하지(flaky) 않도록 전체 데이터베이스를 정리한다. 위 코드 예에서는 단순히 truncate table을 사용하지만, 더 복잡한 시스템에서는 이러한 "데이터베이스 리셋" 로직을 별도의 특수 클래스로 추출해야 할 수도 있다.
  • 각 클래스가 끝나면 연결 누수 방지를 위해 데이터베이스 연결을 닫는다. 이 예에서는 간단한 connection.close로 충분하지만, 현실에서는 전문적인 연결 풀(connection pool)을 사용해야 할 수도 있다(테스트 코드뿐만 아니라 생산 코드에서도).
  • save 테스트 메쏘드가 save()와 all() 메쏘드를 모두 실행한다. 이게 데이터베이스에 값을 삽입하고, 이후에도 계속 올바르게 저장되어 있는지 확인한다.
  • atLeast 테스트 메쏘드가 allWithAtLeast() 메쏘드를 실행한다. 이 테스트가 value>=? 조건의 경계 값들을 실행하는 것을 알 수 있다.  
  • 테스트 데이터 빌더(이 경우 InvoiceBuilder 클래스)가 테스트 데이터를 빠르게 구축하는 데 도움이 된다.

 

반응형

+ Recent posts