티스토리 뷰
먼저 읽어볼만한 글
라이브러리 종속성 추가
/build.gradle 파일에 아래 내용을 추가한다.
dependencies {
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test'
testCompile group: 'org.assertj', name: 'assertj-core', version: '3.12.2
}
spring-boot-starter-test
Starter POM의 추가 만으로 Spring Test, JUnit, Hamcrest, Mockito를 모두 사용하여 테스트 클래스를 작성할 수 있다.
테스트 클래스 작성
/src/test/java/com.jsonobject.example/ExampleTest.java 파일을 아래와 같이 작성한다.
import com.jsonobject.example.Application;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@TestPropertySource(properties = "server.port=8081")
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ActiveProfiles
public class ExampleTest {
@Autowired
@Qualifier("anotherDAO")
private SomeDAO someDAO;
@Resource(name = "anotherService")
private SomeService someService;
@Autowired
private SomeController someController;
@Test
public void aTest() {
// 테스트 코드 작성
}
@Test
public void bTest() {
// 테스트 코드 작성
}
}
- 클래스 레벨에
@RunWith(SpringRunner.class)
,@SpringBootTest
를 명시하면, 실제 구동 환경과 동일하게 애플리케이션의 ApplicationContext를 완전하게 테스트할 수 있다. 다만, 애플리케이션의 실제 초기 구동과 완전히 같은 방법으로 ApplicationContext을 생성하기 때문에, 테스트 준비에 상당한 시간이 소요된다. 따라서, @SpringBootTest(classes = {FooRepository.class, BarService.class})와 같이 테스트에 사용되는 클래스만 명시하면, 테스트 시간을 절약할 수 있다. 비슷한 예로, @SpringBootTest 대신@DataJpaTest
을 명시하면 @Repository 빈만 로드한다. [관련 링크] - HTTP 요청 테스트 또한 가능하다. webEnvironment 옵션을 지정할 수 있는데 SpringBootTest.WebEnvironment.DEFINED_PORT을 지정하면 application-{profile}.xml 파일에 명시된 server.port 값을 따르게 된다. 즉, 적용된 프로파일과 동일한 포트를 사용하여 유닛 테스트를 수행할 수 있다. 고려할 점은 이미 서비스가 기동 중인 환경에서는 같은 포트를 쓰게 되므로 충돌이 나서 테스트가 불가능하다. 이러한 포트 충돌을 예방하려면 유닛 테스트를 기동할 때 다른 포트를 사용해야 한다. 위 예제의
@TestPropertySource
의 properties 옵션을 사용하여 충돌이 나지 않는 포트를 명시할 수 있다. - HTTP 요청에 의한 테스트 방법은 매우 간단하다.
@Test
가 명시된 메써드 범위에서 로컬로 지정된 포트에 대해 원하는 요청을 실행하여 기대 결과를 확인하면 된다. 내 경우Retrofit
을 사용하면 서버 사이드 코드를 테스트에 재사용할 수 있어 선호하는 편이다. [관련 글] - 클래스에
@ActiveProfiles
를 명시하면 테스트 실행시 적용될 프로파일을 적용할 수 있다. - 메써드에
@Test
를 명시하면 테스트 메써드로 선언되어 메써드 단위 테스트가 가능해진다. 클래스 단위 테스트시 실행될 대상 메써드가 된다. - JUnit에서는 클래스 단위 테스트 실행시 메써드 간의 테스트 순서가 임의로 진행된다. 클래스에
@FixMethodOrder
를 명시하면 테스트 메써드 간의 실행 순서를 결정할 수 있다. MethodSorters.NAME_ASCENDING은 메써드의 이름 순으로 실행 순서를 결정하겠다는 의미이다.
ApplicationContext와 캐시
- 유닛 테스트 과정에서 한번 생성된 ApplicationContext는 캐시되어 테스트가 종료되기 전까지 재사용됨으로서 테스트 시간을 단축할 수 있다. (정확히는 각 ApplicationContext가 서로 다른 Key를 식별자로 ContextCache에 캐시로 저장되어 동일한 ApplicationContext을 사용하는 테스트들은 캐시를 사용하게 된다.) [관련 링크]
- 주의할 점은, 서로 다른 조건의
@SpringBootTest
는 각 조건마다 별개의 ApplicationContext를 생성하여 캐시를 재사용할 수 없게 된다. 즉, 조건이 다양할수록 테스트를 위한 ApplicationContext 생성 시간이 소요되어 전체 유닛 테스트 시간이 길어질 수 있다. (아래 설명할@MockBean
을 사용하는 경우에도 캐시를 사용할 수 없다.)
Assertion 작성
- 유닛 테스트의 기본은 실제 결과가 기대 결과와 일치하는가를 확인하는 것이다. 일치하는가? 부분만 일치하는가? 포함하는가? 특정 값보다 큰가? 또는 작은가? 와 같은 기대 결과를 작성하는 도구를 Assertion 라이브러리라고 부른다. Java 진영에서는 전통적으로
Hamcrest
가 널리 사용되어 왔고 최근AssertJ
가 급속도로 인기를 얻고 있다. 개인적으로 물 흐르듯한 직관적인 문법의 AssertJ를 선호하여 아래 간단히 사용 예를 정리하였다. 앞서 설명한 예제의 @Test 메써드 범위에서 사용이 가능하다.
assertThat(object).isNull();
assertThat(object).isNotNull();
assertThat(value).isEqualTo(expectedValue);
assertThat(value).as("HTTP Status Code").isEqualTo(200);
assertThat(stringValue).hasSize(10);
assertThat(localDateTime).isAfter(today);
assertThat(stringList).contains("Ronaldo", "Messi");
assertThat(stringList).contains("Ronaldo").doesNotContain("Neymar");
assertThat(objectList).extracting("name").contains("Ronaldo");
@MockBean, @SpyBean의 사용
- 유닛 테스트에서 가장 중요한 것 중 하나가 바로 목킹이다. 저장소나 외부 서비스가 연동된 기능에 대한 유닛 테스트는 매번 정확히 의도된 결과를 만들어내기가 어렵다. 따라서 적절한 목킹이 필요하다. Spring Boot는
Mockito
을 사용하여 목킹을 제공한다. 특정 스프링 빈을@MockBean
을 사용하여 목킹하면, 완벽하게 특정 조건에 대해 개발자가 의도한 결과를 반환하도록 통제할 수 있다. 이를 통해 개발자는 외부 변수에 대한 고민 없이 기능 자체에 대한 테스트에 집중할 수 있다. 사용 예는 아래와 같다.
@MockBean
private lateinit var fooRepository: FooRepository
@Before
fun initMock() {
whenever(fooRepository.count()).thenReturn(100)
}
@Test
fun fooCountTest() {
assertThat(fooRepository.count()).isEqualTo(100)
}
whenever().thenReturn()
구문은 Kotlin을 위해 간소화되어 제공되는 목킹을 위한 문법이다. 위 예제와 같이@Before
에 선언하여 복수개의 유닛 테스트가 공유할 수 있으며, 개별@Test
에 선언하여 해당 유닛 테스트에만 적용할 수도 있다.- @MockBean을 통해 주입한 빈은 유닛 테스트에서 직접 호출할 때 뿐만 아니라, 내부 로직에서 해당 빈을 이용하는 다른 빈을 호출할 때에도 적용된다. 이를 통해 개발자는 해당 빈에 대한 기대값을 완벽히 통제할 수 있다.
- 별도의 목킹 없이 @MockBean만 선언했을 경우 어떻게 될까? 기본 타입의 경우 타입에 따라 0, 0.0 등을 반환한다. List<?> 객체의 경우 빈 List 객체를 반환한다.
- @MockBean을 이용한 목킹시 주의할 점은, 해당 어노테이션이 사용된 클래스는 앞서 실행된 테스트 클래스에서 생성된 ApplicationContext 캐시를 사용하지 못한다는 것이다. (제작진은 @MockBean에 의한 기대 행동의 조작이 전체 ApplicationContext의 수정과 동일한 효과를 가지기 때문이라고 설명하고 있다.) [관련 링크]
웹 브라우저 테스트
- 추가적으로 웹 브라우저에서의 동작을 테스트하려면 웹 브라우저 테스팅 프레임워크인
Selenium
과Selenide
라이브러리 정보를 아래와 같이 추가해야 한다.
dependencies {
compile('org.seleniumhq.selenium:selenium-api:3.10.0')
compile('org.seleniumhq.selenium:selenium-chrome-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-edge-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-firefox-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-ie-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-java:3.10.0')
compile('org.seleniumhq.selenium:selenium-opera-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-remote-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-safari-driver:3.10.0')
compile('org.seleniumhq.selenium:selenium-support:3.10.0')
compile('com.codeborne:selenide:4.10')
}
- 셀레니움은 라이브러리 본체와 웹 브라우저 인터페이스로 구성된다. 따라서 실제 웹 브라우저 역할을 수행하는 드라이버가 로컬 시스템에 설치되어야 한다. 본 예제에서는 가장 대중적인
Google Chrome Driver
를 기준으로 설명한다. 여기를 클릭하여 운영체제에 맞는 드라이버를 다운로드한다. - 로컬에 위치한 드라이버 파일은 개인 프로젝트라면 문제가 없지만 다수가 사용하는 테스트 도구라면 항상 바이너리 파일이 특정 위치에 설치되어 있어야 하는 번거로움이 생긴다. 그래서 프로젝트 내의 /src/main/resources 디렉토리에 웹 드라이버 바이너리 파일을 포함하여 배포하고 테스트시 해당 파일을 각 로컬 시스템에 복사하여 사용하도록 구현하는 것이 편리하여 추천한다.
- 웹 브라우저 테스트 목적으로는 설치된 웹 드라이버와
Selenium
만으로도 충분하다. 하지만Selenide
를 사용하면 철저히 브라우저를 사용하는 사용자의 느낌으로 쉽고 직관적인 테스트 코드 작성이 가능해진다. 마치Javascript
와jQuery
의 관계와 같다. - 본격적으로 테스트 코드를 작성하기 전에 편의를 위해 자주 변경될 가능성이 높은 옵션을 환경설정 파일로 분리시키는 것이 좋다. /src/main/resources/application.yml 파일을 아래와 같이 작성한다.
selenide:
browserType: chrome
browserLocation: chromedriver.exe
headlessMode: true
- browserType에 사용할 웹 브라우저의 종류를 명시하고 browserLocation에는 앞서 /src/main/resources 디렉토리에 복사한 바이너리 파일의 경로를 명시한다. 마지막으로 headlessMode 여부를 명시한다. true일 경우 테스트 과정에서 웹 브라우저 화면이 나타난다. false일 경우 눈에 보이지 않게 백그라운드로 진행된다.
- 드라이버 설치까지 완료되면 아래와 같이 웹 브라우저 테스트 클래스를 작성할 수 있다.
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ActiveProfiles
@Slf4j
public class ExampleTest {
@Value("${selenide.browserType}")
private String BROWSER_TYPE;
@Value("${selenide.browserLocation}")
private String BROWSER_LOCATION;
@Value("${selenide.headlessMode}")
private boolean HEADLESS_MODE;
@Before
public void INIT() throws IOException {
// 프로젝트에 포함된 WebDriver 바이너리 파일을 테스트를 실행한 로컬 시스템에 복사
if (!Files.exists(Paths.get(BROWSER_LOCATION))) {
FileUtils.copyFile(new ClassPathResource(BROWSER_LOCATION).getFile(), new FileOutputStream(BROWSER_LOCATION));
}
Configuration.browser = BROWSER_TYPE;
Configuration.headless = HEADLESS_MODE;
System.setProperty("webdriver.chrome.driver", BROWSER_LOCATION);
}
@Test
public void GET_GOOGLE_SEARCH_BUTTON_TEXT() {
open("https://google.com");
$("input[value=\"I’m Feeling Lucky\"]").should(exist);
}
}
Gradle 테스트하기
- Gradle 기반의 프로젝트는 아래와 같은 명령어로 앞서 작성한 유닛 테스트를 실행할 수 있다.
# 테스트 후 빌드 진행, 테스트 실패시 빌드 불가
$ gradle build
# 테스트를 생략하고 빌드 진행
$ gradle build -x test
# 테스트만 진행
$ gradle test
# 프로파일 변수로 test를 전달하여 테스트 진행
$ SPRING_PROFILES_ACTIVE=test gradle test
- 테스트가 종료되면 결과 리포트 HTML 파일이 /build/reports/tests/test/index.html 경로에 생성되어 확인이 가능하다.
- 한편, 테스트 결과로 생성되는 파일과 별개로 콘솔로 실시간으로 출력되는 테스트 결과의 테마를 지정할 수 있다. Gradle 플러그인으로 제공되는
com.adarshr.test-logger
를 사용하는 것인데 /build.gradle 파일에 아래 내용을 추가하면 된다. 테마는 plain, standard, mocha 3가지가 제공된다. 가장 가독성이 뛰어난 것은mocha
테마이다.
plugins {
id "com.adarshr.test-logger" version "1.1.2"
}
testlogger {
theme 'mocha'
}
- 콘솔 출력시 로거에 의해 출력되는 로그는 오히려 방해가 된다. /src/test/resources/logback-test.xml 파일을 아래와 같이 작성하면 테스트 진행 중에 로그가 출력되지 않는다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration />
Gradle 코드 커버리지
- 테스트 결과 못지 않게 중요한 것이 Code Coverage 확인이다. Gradle는 코드 커버리지 기능의
JaCoCo
플러그인을 제공한다. 먼저 프로젝트의 /build.gradle 파일에 아래 내용을 추가한다.
apply plugin: 'jacoco'
- 아래와 같이 테스트 결과에 대한 코드 커버리지를 확인할 수 있다. 반드시 코드 커버리지에 앞서 테스트가 선행되어야 한다. 리포트 HTML 파일은 /build/reports/jacoco/test/index.html 경로에 생성되어 확인이 가능하다.
$ gradle test jacocoTestReport
참고 글
댓글
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 구동계
- Docker
- bootstrap
- jstl
- MySQL
- 로드 바이크
- 로드바이크
- java
- CentOS
- Eclipse
- graylog
- node.js
- Spring Boot
- 평속
- jpa
- jsp
- 태그를 입력해 주세요.
- Spring MVC 3
- Kendo UI
- Kendo UI Web Grid
- DynamoDB
- maven
- Tomcat
- 알뜰폰
- JavaScript
- spring
- 자전거
- chrome
- JHipster
- kotlin
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함