본문 바로가기
개발 공부

[개발 공부] Spock Test Framework

by 모닥불꽃 2021. 7. 20.
반응형

이번 포스팅에서는 인턴을 시작하면서 받은 교육 중, Spock Test Framework에 대해 정리해보려 합니다.

Java를 사용한 개발을 진행하면서 들어본 테스트는 JUnit이라는 것인데, Spock Framework는 생소한 기술이었습니다.


왜 테스트를 해야 하는가?

실제 운영 중인 서비스에서, DB의 백업이 되고 있지 않거나, 개발 중인 소스 코드를 VCS(Version Control System)을 사용하여 백업을 하고 있지 않는 경우엔 어떻게 될까요?

개발하다가 한 번의 실수를 하면 복구도 못하고, 관리에 어려움을 느낄 수 있기 때문에 DB 백업과 VCS를 통한 백업을 진행합니다.

 

그렇다면 테스트 없이 개발을 하면 어떻게 될까요?

마찬가지로 좋은 코드를 작성할 수 없고 제대로 개발하기 힘듭니다.

 

개발자는 책임을 갖고 좋은 테스트 코드를 작성해야 합니다.

좋은 테스트 코드란, 단순하게 그냥 돌아가는 테스트 코드가 아닌, 여러 가지 상황에 따른 좋은 테스트 코드를 말합니다.

좋은 테스트 코드를 한번 작성해두면, 추후에 리팩터링 할 때도 쉽게 할 수 있습니다.


단위 테스트의 FIRST 원칙

Fast

테스트 코드에 대한 속도는 빨라야 합니다.

테스트 코드를 아무리 잘 작성해서 예외처리 등을 잘 검토해도, 수행 시간이 오래 걸리면 테스트를 자주 수행하지 않을 것입니다.

테스트를 자주 수행하지 않으면 해당 코드의 신뢰도와 품질이 떨어지게 됩니다.

Isolation

각 테스트는 서로 독립적이어야 합니다.

하나의 테스트가, 다른 테스트의 결과에 의존적이면 안됩니다.

테스트는 독립적으로, 순서에 상관없이 수행할 수 있어야 합니다.

만약 테스트가 독립적이지 않고 의존이 생기면, 하나의 테스트가 실패하면 연속적으로 실해하기 때문에 진단하기 어렵습니다.

즉, 테스트의 직관성이 떨어집니다.

Repeatable

테스트는 반복적으로 실행해도 같은 결과가 나와야 합니다.

또한 테스트는 다른 시점, 다른 환경(ex. 테스트 환경, QA 환경, 개발 환경)에서도 모두 동일한 결과를 내야 합니다.

Self-validation

테스트의 결과는 성공 or 실패 중 1개의 결과를 내야 합니다.

또한 결과는 assert 등을 통해 IDE/HTML 등의 형태로 받아야 합니다.

로그 파일이나 출력으로 확인해서는 안 됩니다.

Timely

프로덕션 코드가 작성되거나 변경이 완료된 시점에 즉시 테스트되어야 합니다.

만약 개발에 TDD를 도입한다면 다음과 같은 과정을 거칠 것입니다.

  1. 프로세스의 시나리오대로 테스트 코드 작성
  2. 테스트 코드를 성공적으로 통과하기 위한 프로덕션 코드 작성
  3. 테스트 수행 반복

단순히 하나의 기능을 개발하기 위해 이 과정을 거치면 개발 시간에 오래 걸릴 것이라는 생각을 할 수 있습니다.

하지만 TDD를 통해 개발하면 오류를 더 빨리 잡고, 추후 리팩터링을 비교적 쉽게 할 수 있습니다.

그래서 전체적인 개발 기간은 줄어들게 됩니다.


Spock Framework란?

Spock은 Java 및 Groovy를 위한 테스트 프레임워크입니다.

내부적으로 JUnit Runner로 구동되며, 대부분의 IDE에서 지원됩니다.

Spock의 공식 문서를 보면 다음과 같은 문장을 확인할 수 있습니다.

Spock lets you write specifications that describe expected features (properties, aspects) exhibited by a system of interest.

해당 문장을 해석해보면 다음과 같이 이해할 수 있습니다

Spock은 현재 개발 중인 것이 만들어내는 결과의 명세를 작성하게 해 줍니다.

이 문장의 뜻은 즉, 현재 개발 중인 자바 코드나 애플리케이션이 정상적인 동작을 하도록 일종의 명세서를 작성하는 것이 Spock Framework라는 뜻입니다.


Specification

테스트 코드를 작성하는 클래스가 Specifiacion을 상속받으면 테스트 클래스라고 인식됩니다.

아래 사진처럼 OrderSeviceTest는 Specification을 상속받게 되어 테스트 클래스라고 인식됩니다.

앞서 언급했던 것처럼 Spock Framework는 일종의 명세서를 작성하는 것입니다.

Specification클래스에는 이런 명세서를 작성하기 위해 유용한 메서드를 포함하고 있습니다.

추가적으로, Specification클래스는 JUnit에게 이 명세를 Sputnik(Spock의 JUnit runner)을 기반으로 실행하도록 합니다.

이 Sputnik 덕분에 Spock 명세들이 많은 IDE에서 지원이 됩니다.


Fields

테스트 클래스의 도입부에 작성되는 것으로, 테스트에서 사용되는 것들을 선언하는 부분입니다.

이들은 인스턴스 변수와 동일한 개념이고 테스트마다 초기화됩니다.

이렇게 명세의 초기에 fields를 선언해 두는 것은 가독성도 좋게 만들어줍니다.

선언된 fields들은 각각의 객체를 가지며 서로들로부터 feature methods를 분리시켜줍니다.

 

하지만 feature methods에 fields를 공유하거나, 다른 테스트 코드에서 fields를 사용하기 위해 공유를 필요로 할 수도 있습니다.

이럴 때는 field위에 @Shared라는 어노테이션을 추가해주면 됩니다.

@Shared 어노테이션 외에도 변수를 static으로 선언해주는 방법도 있습니다.

하지만 @Shared어노테이션을 이용해 공유 변수를 만들어야 직관적으로 "아 이 변수는 공유가 되겠구나!"를 알 수 있고 가독성도 높아집니다.

static선언은 상수를 선언할 때만 사용하기를 권장하고 있습니다.


Fixture Methods

Fixture methods들은 feature methods들이 실행될 환경을 설정, 세팅, 초기화하는 데 사용됩니다.

모든 fixture methods들은 선택사항으로 사용하거나, 하지 않아도 됩니다.

 

가끔, feature methods들이 fixture methods를 공유하곤 합니다.

setupSpec()과 cleanupSpec()을 통해 공유 변수들을 설정하고 초기화하곤 합니다.

(@Shared 어노테이션이 붙어있는 변수들만 관리할 수 있음을 주의해야 합니다)

Fixture methods들은 다음과 같은 수행 순서를 갖고 있습니다.

  1. super.setupSpec
  2. sub.setupSpec
  3. super.setup
  4. sub.setup
  5. feature method //실제로 돌아가는 테스트 대상 메서드들
  6. sub.cleanup
  7. super.cleanup
  8. sub.cleanupSpec
  9. super.cleanupSpec

위 fixture methods 수행 순서에서 3~7번은 테스트가 수행될 때마다 반복적으로 실행됩니다.


Feature Methods

이제 Spock 테스트 코드의 핵심인 feature methods입니다.

Feature methods가 feature methods로 불리는 이유는 다음과 같습니다.

Spock Framework의 공식 문서를 확인해보면 다음과 같이 명시되어있습니다.

They describe the features (properties, aspects) that you expect to find in the system under specification.

이 문장을 해석해보면 다음과 같습니다

이 명세를 통해 나오는 기댓값들, 특징(feature)을 설명합니다.

즉 feature methods를 직역하면 "특징 함수"로, 테스트 코드를 작성해 원하는 특징들을 설명할 수 있는 기능을 갖고 있습니다.

Feature methods를 선언할 때 def "테스트 이름"()으로 선언합니다.

Feature methods의 가장 큰 특징은, 이름을 문자열 형태로 작성하며, 해당 테스트가 어떤 걸 테스트하는지 명확하게 작성할 수 있습니다.

내부적으로 block들을 배치하여 다음과 같은 4개의 구문으로 수행됩니다.

  1. Feature 초기화 -Setup (선택 사항)
  2. 테스트 수행 - Stimulus
  3. 응답 비교 - Response
  4. 클리어 - Cleanup (선택 사항)

Blocks

앞서 언급한 feature method의 4개 구문을 달성하기 위해 spock에서 내부적으로 갖고 있는 것이 있습니다.

그게 바로 blocks입니다.

Feature method는 이 block들 중 하나는 필수적으로 있어야 합니다.

Spock에는 6가지의 block이 있고, 이것들이 위 4개의 구문을 수행하기 위해 각각의 역할은 다음과 같습니다.

  • given = Setup
  • when = Stimulus
  • then = Response
  • expect = Stimulus + Response (when + then과 동일)
  • cleanup = Cleanup
  • where = 전체적으로 다 수행

아래 이미지가 Spock Framework 공식 문서에 있는 사진입니다.

출처: https://spockframework.org/spock/docs/2.0/spock_primer.html 

 

그럼 이제 본격적으로 block들에 대해 파헤쳐봅시다.

(Block에 대해 설명하는 베이스 테스트 코드는 스택에 원소를 넣는 테스트 코드로 작성할 예정입니다)

 - Given Blocks

Given Blocks는 테스트 수행 준비과정을 명시하는 블록으로 초기화를 담당하고 있습니다.

반복되지 않으며 optional 합니다.

아래 사진처럼 테스트를 수행할 때 필요한 스택 1개, 그리고 원소 1개를 선언해 주었습니다.

- When / Then Blocks

When / Then 블록은 항상 쌍으로 존재합니다.

When 블록에서 테스트 코드가 수행되고, Then 블록에서 테스트 코드 결과의 평가가 진행됩니다.

평가하는 조건들은 5개 이내로 작성하는 것이 바람직합니다.

만약 5개 이상으로 넘어가면, 테스트를 분리하거나 데이터 테이블의 사용을 고려해야 합니다.

그럼 다음 사진처럼 When / Then 블록을 작성해 보고 테스트를 실행시켜 보겠습니다.

When : Stimulus로, 스택에 elem을 하나 넣는 테스트 코드 수행

Then : Response로 When 블록의 수행 결과로 평가 진행

            스택의 크기가 2인지, 스택에 elem이 들어있는지 평가

테스트 코드의 수행 결과는 다음과 같이 나왔습니다.

이 수행 결과는 Spock Framework의 특징입니다.

JUnit테스트와 달리, 실패한 내용을 상세하게 나타냅니다.

결과에서 볼 수 있듯이, stack.size() == 2의 구문에서 정상 값이 1이라 false로 테스트가 실패한 것을 확인할 수 있습니다.

 

수행 결과의 장점 외에도 예외처리에서 JUnit보다 좋은 점을 찾을 수 있습니다.

아래의 예시는 동일한 예외처리를 하고 있습니다.

JUnit의 경우 예외처리를 하려면 어노테이션을 적어야 가능해서 Spock의 예외처리가 더 직관적이고 가독성이 높다는 장점이 있습니다.

Spock Framework에서는 When 블록에서 예외처리를 할 때 사용하고, thrown() 메서드를 사용합니다.

위 예시처럼 예외처리 타입을 명확히 지정함으로 IDE의 도움을 받을 수 있습니다.

 

가끔 테스트 코드를 작성하다 보면, 예외처리 확인뿐만 아니라, 예외처리를 던지면 안 되는 상황을 테스트할 수 있습니다.

HashMap이 null 키를 받아도 된다는 테스트 코드를 작성해 봅시다.

다음 코드를 한번 봅시다.

이 테스트 코드를 실행하면 정상적으로 실행되지만, 이 테스트 코드의 목적 및 조건 등을 알기 어렵습니다.

그럼 이 코드를 한층 개선해 보겠습니다.

테스트 코드를 이렇게 개선하고, notThrown 메서드를 사용하면서, 어떤 예외처리가 발생하면 안 되는지 명시할 수 있습니다.

- Expect Blocks

Expect 블록은 조건이랑 변수 정의만 포함할 수 있다는 점에서 then 블록보다 제한적입니다.

Expect 블록은 stimulus랑 response를 한 줄로 표현할 때 효과적입니다.

다음과 같은 예시를 보면 바로 이해할 수 있습니다.

10과 20중 큰 수를 테스트하는 코드인데, when / then 블록을 사용하면 위의 사진처럼 작성해야 하지만, expect 블록을 사용하면 아래 사진처럼 한 줄로 작성할 수 있습니다.

총정리로, 부가적인 부작용을 확인해야 할 때는 when / then 블록을 사용하고, 순수 함수 테스트를 진행할 땐 expect 블록을 사용합니다.

- Cleanup Blocks

Cleanup 블록은 테스트를 수행할 때 사용했던 파일 시스템, DB Connection, 네트워크 연결 등의 자원을 지워주는 블록입니다.

- Where Blocks

Where 블록은 feature method의 마지막에 위치합니다.

마지막에 위치해 data-driven feature method를 작성하기 위해 사용됩니다.

이 또한 예시를 보면 이해하는데 도움이 될 수 있습니다.

위 코드를 보면 expect 블록에서 Math.max(a, b) == c에 대한 식을 테스트한다고 명시합니다.

해당 a, b, c에 대한 값들을 where 블록에서 반복적으로 돌면서 값을 집어넣습니다.

첫 번째 테스트에  a = 5, b = 1, c = 5가 들어가고, 두 번째 테스트에 a = 3, b = 9, b= 9 가 들어가면서 테스트가 수행됩니다.

이렇게 데이터를 넣는 방법을 Data Pipes라고 합니다.

그럼 마지막 c를 3으로 바꿔볼까요?

위 결과처럼 a, b 가 각각 3,9라서 왼쪽 값이 9인데, 오른쪽에 c가 3이라 불일치로 테스트가 실패하는 것을 볼 수 있습니다.

그럼 다른 방법으로 Where 블록을 구성하는 방법을 보겠습니다.

이렇게 데이터를 Data Table로도 만들어서 넣을 수 있습니다.


Helper Methods

가끔 테스트 코드를 작성하다 보면 feature 메서드가 너무 길어지거나 반복되는 코드가 작성되는 경우가 있습니다.

이때 사용하는 것이 helper 메서드입니다.

Helper 메서드는 setup/cleaner 메서드나 복잡한 조건을 쉽게 작성하는 데 사용됩니다.

먼저, Shop이라는 클래스를 다음과 같이 정의했습니다.

vendor는 Sunny, clockRate는 2000, ram 은 4000, os는 Windows를 탑재한 pc를 리턴하는 buyPc() 메서드도 있습니다.

다음과 같이 테스트 코드를 작성해 보겠습니다.

이 테스트 코드를 실행하면, vendor는 일치하지만, clockRate는 2000이라 2333 이상을 요구하는 테스트 코드에서 실패를 일으킵니다.

clockRate에서 테스트 코드의 실패가 일어나서, 그 후의 ram, os 등은 검사를 건너뛰는 것을 확인할 수 있었습니다.

Helper 메서드를 통해 테스트 코드의 then 블록의 로직을 하나의 함수로 분리할 수 있습니다.

Then 블록의 로직을 matchesPreferredConfiguration 메서드로 따로 분리했습니다.

이는 테스트 코드의 재사용을 위해 분리된 것입니다.

실행 결과를 보면 마찬가지로 조건이 맞지 않아서 테스트 코드가 실패한 것을 볼 수 있습니다.

하지만 이 결과는 저희의 목적을 달성시킬 수 없습니다.

만들어진 pc에서 어느 정보가 틀렸는지 알 수 없고, 단지 결과가 틀렸다는 것만 알 수 있습니다.

그럼 테스트 코드에서 작성한 matchesPreferredConfiguration 메서드를 다음처럼 수정해 보겠습니다.

이제 테스트 코드를 다시 실행하면 다음과 같은 결과를 얻을 수 있습니다.

결과는 아까 작성했던 함수로 분리하기 전의 결과와 같이 clockRate에서 조건에 부합하지 않아 fail을 결과로 내고 테스트를 중단했습니다.

첫 번째 helper 메서드보다 두 번째처럼 작성하는 게 더 바람직합니다.

Helper 메서드로 테스트 코드를 분리할 때에는 주의할 점이 두 가지입니다.

  1. assert 키워드로 조건을 테스트해라
  2. 리턴 타입으로 인해 테스트 결과가 달라질 수 있으므로 만일을 대비해 리턴 타입은 void형으로 해라

물론 이렇게 테스트 코드를 분리해서 재사용을 할 수 있게 구성하면 좋지만, 너무 과한 경우 fixture와 helper 메서드의 종속성을 높게 해서 유지보수가 힘들어질 수도 있습니다.


With 키워드 사용

위에서 진행했던 컴퓨터 사양 테스트와 동일한 테스트를 진행하겠습니다.

이번에는 then 블록에 with 키워드를 사용해 작성해 보겠습니다.

이렇게 수정한 테스트 코드를 실행하면, 기존의 테스트 코드처럼 어디서 틀렸는지 알려주는 결과를 받을 수 있습니다.

With키워드를 사용하면서 조건을 비교할 때 pc.vendor처럼 작성 안 해도 돼서 코드가 깔끔해졌습니다.

또한 assert문 또한 필요가 없어졌습니다.


Verifyll 키워드 사용

여태까지 테스트 코드를 실행하고 결과를 확인하면서, 컴퓨터의 4가지 조건중 2번째 조건을 만족하지 못하면 테스트를 바로 종료하는 것을 확인할 수 있었습니다.

이렇게 되면, 그 이후의 값들에 대해서는 테스트를 전혀 하지 못하는 단점이 있습니다.

이를 해결해 주는 것이 verifyAll 키워드입니다.

그럼 verifyAll 키워드를 사용해서 테스트 코드를 수정해보겠습니다.

그리고 해당 테스트 코드를 실행시켜 보겠습니다.

PC의 clockRate가 딱 2000이라서 테스트 코드의 조건인 2000 초과에 부합하지 않은 것을 첫 번째 결과에서 확인할 수 있습니다.

PC의 os가 Windows여서 테스트 코드가 조건인 Linux와 부합하지 않은 것을 두 번째 결과에서 확인할 수 있습니다.

이렇게 실패한 경우 정지하지 않고 테스트 전체를 수행하고, 어떤 게 실패했는지 마지막에 알려주면서 한 번에 에러를 다 찾을 수 있다는 장점을 갖고 있습니다.


Documentation

여태까지 테스트 코드의 구성과 작성 법에 대해 익혀봤습니다.

좋은 테스트 코드는 여러 가지 테스트를 하거나 빨라야 하지만, 누구나 알아볼 수 있는 문서화도 중요합니다.

테스트 코드를 작성할 때, 해당 블록이 어떤 일을 하는지 옆에 문자열 형태로 명시할 수 있습니다.

선택적인 옵션이긴 하나, 문서화 및 표시를 잘해두면 좋은 테스트 코드가 될 수 있습니다.

위 사진처럼 테스트 코드를 작성하며 블록 옆에 문자열 형태로 해당 블록이 하는 일을 명시할 수 있습니다.

중간에 있는 처음 보는 and 블록은 바로 위에 있는 블록의 역할을 합니다.

첫 번째와 두 번째 and는 given의 역할을 합니다.

세 번째 and는 when의 역할을 합니다.


JUnit과의 차이

이 사진은 Spock Framework와 JUnit의 차이점을 정리해 둔 표로, Spock 공식 문서에서 확인할 수 있습니다.


이렇게 여기까지 Spock Framework에 대해 기본적인 것을 알아보았습니다.

다음 포스팅에서는 실제 코드를 작성하고 테스트 코드를 작성하는 것을 실습해 보려고 합니다.

반응형

댓글