티스토리 뷰

먼저 읽어볼만한 글

라이브러리 종속성 추가

/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 값을 따르게 된다. 즉, 적용된 프로파일과 동일한 포트를 사용하여 유닛 테스트를 수행할 수 있다. 고려할 점은 이미 서비스가 기동 중인 환경에서는 같은 포트를 쓰게 되므로 충돌이 나서 테스트가 불가능하다. 이러한 포트 충돌을 예방하려면 유닛 테스트를 기동할 때 다른 포트를 사용해야 한다. 위 예제의 @TestPropertySourceproperties 옵션을 사용하여 충돌이 나지 않는 포트를 명시할 수 있다.
  • 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 BootMockito을 사용하여 목킹을 제공한다. 특정 스프링 빈을 @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의 수정과 동일한 효과를 가지기 때문이라고 설명하고 있다.) [관련 링크]

웹 브라우저 테스트

  • 추가적으로 웹 브라우저에서의 동작을 테스트하려면 웹 브라우저 테스팅 프레임워크인 SeleniumSelenide 라이브러리 정보를 아래와 같이 추가해야 한다.
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를 사용하면 철저히 브라우저를 사용하는 사용자의 느낌으로 쉽고 직관적인 테스트 코드 작성이 가능해진다. 마치 JavascriptjQuery의 관계와 같다.
  • 본격적으로 테스트 코드를 작성하기 전에 편의를 위해 자주 변경될 가능성이 높은 옵션을 환경설정 파일로 분리시키는 것이 좋다. /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
    

    참고 글

TAG
,
댓글
댓글쓰기 폼