에플리케이션 테스트란 특별한 또 다른 과정이 아니라, 스프링을 만들어보고 예제를 실행해 가면서, 출력 로그
, HTTPResponseBody확인
,데이터베이스 테이블 확인
등 구현한 애플리케이션이 예상한 값을 출력하는지 확인하는 모든 과정을 테스트 과정이라고 합니다.
지금은 학습하는 입장이라 구동시켜보고 테스트 해보는 것이 어려운 일이 아니지만 다양한 기능을 제공하게 되고 여러 사용자를 처리해야 될 정더로 애플리케이션이 커지면 그때마다 실행해서 확인하는 것은 쉬운일이 아닐 것입니다.
단위 테스트에서 단위
에 대하여 생각하기 위해 어떤 기준으로 정해야 할지 생각해 봐야 합니다.
기능 테스트
: 테스트의 범위중 가장 크고, 가장 큰 단위입니다. 애플리케이션을 이용하는 사용자입장에서 애플리케이션이 제공하는 기능이 올바르게 동작하는지를 테스트합니다.
주로 전문 QA가 테스트하며 종종 프론트엔드 개발자도 테스트를 진행하기도 합니다. 구현한 애플리케이션과 연관된 대상이 많기 때문에 단위 테스트로 부르기는 힘듭니다.
통합테스트
: 클라이언트 측 툴 없이 개발자까 짜 놓은 테스트 코드를 실행시켜서 이루어 지는 경우입니다. 클라이언트 사이드 와는 관련이 없어지지만, 여전히 다른 계층과 데이터베이스와는 관계가 있으므로 단위 테스트라고 할 수 없습니다.
슬라이스 테스트
: API계층, 서비스 계층, 데이터 엑세스 계층이 각각 슬라이스 테스트 대상이 되므로 각각 다른 계층관 관련은 없어지지만, 외부 HTTP요청이나, 데이터베이스, 외부 서버와 연동되기도 하므로 단위 테스트라고 볼 수 없습니다.
단위 테스트라고 하기엔 외부와 연결되어 있고, 통합 테스트라고 하기엔 계층별로 나뉘어 져 있으므로 부분 통합 테스트 라고도 합니다.
단위 테스트
: 계층별로도 나뉘어져 있고, 외부 연결과도 독립거으로 작동하는, 비즈니스 로직에서 사용하는 클래스들이 독립적으로 테스트하기 가장 좋은 대상이므로 보통 단위 테스트 코드는 메서드 단위로 대부분 작성 됩니다.데이터베이스의 상태가 테스트 이전과 이 후가 동일하게 유지될 수 있다면 데이터베이스가 연동되어도 단위 테스트라고 할 수 있습니다.
테스트 케이스(Test Case)
: 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세서 입니다.단위 테스트 작성에 참고할 수 있는 가이드 로 F.I.R.S.T
원칙이 존재합니다.
F.I.R.S.T | 설명 |
---|---|
Fast(빠르게) | 테스트 케이스는 빠르게 실행할 수 있어야 합니다. |
Independent(독립적) | 어떤 테스트 케이스가 먼저 실행되어도 실행 순서와 상관이 없이 각각의 테스트 케이스는 독립적이어야 합니다. |
Repeatable(반복 가능) | 어떤 환경에서든 반복해서 같은 결과를 확인할 수 있어야 합니다. 외부 리소스가 연동될 경우 테스트 결과에 영향을 줄 수 있어 끊어주는 것이 바람직합니다. |
Self-validating(셀프 검증) | 단위 테스트는 성공 또는 실패 결과를 보여주어야 합니다. |
Timely(시기 적절) | 구현하고자 하는 기능들을 업그레이드 할 때 마다 그때 그때 테스트 케이스 역시 단계적으로 업그레이드 해야 합니다. |
BDD(Behavior Driven Development)
라는 테스트 방식에서 사용하는 방법입니다.
가독성을 높이고 테스트 케이스를 짜임새 있게 구성하는 것을 도울 수 있습니다.
검증(Assertion)
합니다.Assertion은 테스트 결과를 검증할 때 주로 사용합니다.
테스트 결과가 참이길 바란다
라는 의미로 해당 결과가 참인지를 비교합니다.
Spring에선 기본적으로 JUnit 테스트를 지원합니다.
SpringBoot Initializer로 프로젝트 생성시 build.gradle
에 testImplementation 'org.springframework.boot:spring-boot-starter-test'
가 포함되어 있으며 JUnit
도 포함되어 있습니다.
Junit은 기본적으로 src/main/java/...
경로의 패키지가 아닌 src/test/java/...
경로의 패키지에 존재합니다.
public class 테스트{
@Test
public void 테스트1(){
}
@Test
public void 테스트2(){
}
@Test
public void 테스트3(){
}
}
단위 테스트는 메서드 단위로 작성한다는 것을 기억하시나요?
@Test
에너테이션이 있는 메서드에 테스트 로직을 작성하면 테스트 할 수 있습니다.
@DsiplayName("테스트 케이스 이름")
@Test
public void 테스트(){
...
assertEqauls(expected,actual);
}
@DisplayName("테스트 케이스 이름")
: 테스트 수행시 해당 테스트의 이름을 지정할 수 있습니다.
assertEquals(expect,actual)
: expected
변수와 actual
변수가 값이 동일하면 passed
, 다르면 failed
을 반환합니다, assertXXX
형식으로 다양한 어써션 메서드를 JUnit은 제공합니다.
@DsiplayName("테스트 케이스 이름")
@Test
public void 테스트(){
...
assertNotNull(변수,"에러 시 출력 메시지");
}
assertNotNull(변수,"에러 시 출력 메시지")
: 변수가 Null이면 failed
을 반환하고 지정한 메시지를 출력합니다. Null이 아니면 passed
를 반환합니다. @DsiplayName("테스트 케이스 이름")
@Test
public void 테스트(){
...
assertThrows(예외Exception.class, () -> ....);
}
assertThrows(예외Exception.class, () -> ...)
: 람다식에서 첫번째 매개변수로 지정한 예외가 발생하면 passed
, 에러가 발생하지 않거나 발생하더라도 지정한 예외를 상속받지 않는 다른 예외가 발생하면 failed
를 반환합니다.여기서 예외Exception.class
를 지정하지만 만약 람다식에서 예외Exception.class
를 상속받는 상속Exception.class
가 발생하였다면 passed
를 반환합니다. 예외Exception.class
를 상속받지 않은 다른Exception.class
예외가 발생하였다면 failed
을 반환합니다.
테스트를 수행하는 클래스는 테스트를 수행하는 @Test
에너테이션 말고도 다른 에너테이션을 사용할 수 있습니다.
에너테이션 | 설명 |
---|---|
@BeforeEach | 테스트 케이스마다 초기화 작업을 실행합니다. |
@BeforeAll | 모든 테스트를 시작전 초기화 작업을 한번만 실행합니다. |
@AfterEach | 테스트 케이스마다 종료 작업을 실행합니다. |
@AfterAll | 모든 테스트케이스 종료 후 종료 작업을 한번만 실행합니다. |
public class 테스트{
@BeforeEach
public void initEach(){
System.out.println("BeforeEach");
}
@BeforeAll
public void initAll(){
System.out.println("BeforeAll");
}
@AfterEach
public void endEach(){
System.out.println("AfterEach");
}
@AfterAll
public void endAll(){
System.out.println("AfterAll");
}
@Test
public void test1(){
}
@Test
public void test2(){
}
@Test
public void test3(){
}
}
각각의 전처리 후처리 에너테이션들과 , 테스트케이스 3개를 실행해보면 다음의 결과와 같습니다.
BeforeAll
BeforeEach
AfterEach
BeforeEach
AfterEach
BeforeEach
AfterEach
AfterAll
BeforeAll이 가장 먼저 실행되고 그다음 각 테스트케이스 전,후로 BeforeEach 와 AfterEach가 실행됩니다. 마지막으로 AfterAll이 실행되고 테스트가 종료됩니다.
Assumption은 ~로 가정한다 할때
가정
에 해당합니다.
public class 테스트{
@DisplayName("테스트 케이스 이름")
@Test
public void 테스트케이스1(){
assumeTrue(매개변수);
...
assertTrue(..);
}
}
assumeTrue(매개변수)
: 주어진 매개변수가 true
면 assumeTrue()
아레의 나머지 로직들을 실행하고, false
면 아래의 나머지 로직들이 실행되지 않습니다.
특정 조건에서만 작동하는 선택적인 테스트 케이스가 필요할때 유용하게 사용될 수 있습니다.
Hamcrest는 JUnit 기반의 단위 테스트에서 사용할수 있는 프레임워크입니다.
Junit도 Assertion
의 다양한 메서드를 사용할 수 있지만 Hamcrest
가 더 많이 사용됩니다.
매처(Matcher)
가 자연스러운 문장으로 가독성이 향상 됩니다.Matcher
를 제공합니다.public class 테스트{
@DisplayName("테스트 케이스 이름")
@Test
public void 테스트케이스(){
String expected = "Test";
String actual = "Hello";
assertThat(actual, is(equalTo(expected));
}
}
Hamcrest
의 assertThat
은 JUnit
의 Assertion
메서드와 유사하지만 차이가 존재합니다.
assertThat(actual, is(equalTo(expected)는 assert that actual is equal to expected(실제값이 예상값과 동일하다고 가정합니다)
라는 하나의 문장으로 자연스럽게 읽혀집니다. 메서드와 매개변수등이 약간 다르고 또 에러 메시지도 조금 다르게 출력합니다.
expected: <Test> but was: <Hello>
Expected :Test
Actual :Hello
Junit의 결과 메시지
Expected: is "Test"
but: was "Hello"
Hemcrest의 결과 메시지
@DisplayName("테스트 케이스 이름")
@Test
public void 테스트케이스1(){
...
assertThat(변수, is(notNullValue()));
}
Hamscret를 사용해서 Not Null 테스트를 하기위해서는 is(), notNullValue() 메서드를 함께 사용할 수 있습니다. 또한 자연스럽게 읽혀질수 있습니다. assert that 변수 is not null value(변수는 null 값이 아니다고 가정합니다.)
@DisplayName("테스트 케이스 이름")
@Test
public void 테스트케이스1(){
Throwable actual = assertThrows(예외Exception.class, () -> ...);
assertThat(actual.getCause(), is(equalTo(null)));
}
Hamscret
만으로 Assertion
구성 하기 힘들기 때문에 JUnit
의 Assertion
메서드를 이요해서 검증을 하기도 합니다.
Junit
의 assertThrows()
를 이용하여 예외 발생 여부를 확인하고 Hamscret
의 assertThat()
을 이용하여 추가로 검증을 진행했습니다.
또 Hasmcret만으로 검증하기 위해서는 Custom Matcher
를 직접 구현해서 사용할 수도 있습니다.
실력이 좋은 개발자 일수록 테스트케이스를 자주 그리고 많이 사용한다는 것을 김영한 저자님께 들었던 기억이 있습니다. 아직 테스트 케이스를 잘 활용한다고 할 수 없지만 기본적이고 또 원리적으로 이해할 수 있는 시간이어서 아주 흡족스러웠습니다.
private!