Loading...
Spring Framework Reference Documentation 7.0.2의 MockMvc and WebDriver의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
이전 섹션들에서는 MockMvc를 raw HtmlUnit API와 함께 사용하는 방법을 살펴보았습니다. 이 섹션에서는 Selenium WebDriver 내의 추가적인 추상화를 사용하여 작업을 더욱 쉽게 만드는 방법을 설명합니다.
이미 HtmlUnit과 MockMvc를 사용할 수 있는데, 왜 WebDriver를 사용하고자 할까요? Selenium WebDriver는 코드를 쉽게 구성할 수 있게 해 주는 매우 우아한 API를 제공합니다.
이것이 어떻게 동작하는지 더 잘 보여 주기 위해, 이 섹션에서는 예제를 살펴봅니다.
| Selenium의 일부이긴 하지만, WebDriver는 테스트를 실행하기 위해<br>Selenium 서버를 필요로 하지 않습니다. |
메시지가 올바르게 생성되는지 확인해야 한다고 가정해 보겠습니다. 테스트에는 HTML 폼 입력 요소를 찾고, 이를 채운 다음 다양한 어서션을 수행하는 작업이 포함됩니다.
이러한 접근 방식은 에러 조건도 테스트하고자 하기 때문에 수많은 개별 테스트를 초래합니다. 예를 들어, 폼의 일부만 채우면 에러가 발생하는지 확인하고자 할 수 있습니다. 폼 전체를 채우면 새로 생성된 메시지가 이후에 표시되어야 합니다.
필드 중 하나의 이름이 “summary”라면, 테스트 내 여러 위치에서 다음과 유사한 코드가 반복될 수 있습니다:
1HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); 2summaryInput.setValueAttribute(summary);
1val summaryInput = currentPage.getHtmlElementById("summary") 2summaryInput.setValueAttribute(summary)
그렇다면 id를 smmry로 변경하면 어떻게 될까요? 이렇게 하면 이 변경 사항을 반영하기 위해 모든 테스트를 업데이트해야 합니다.
이는 DRY 원칙을 위반하므로, 이상적으로는 다음과 같이 이 코드를 자체 메서드로 추출해야 합니다:
1public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) { 2 setSummary(currentPage, summary); 3 // ... 4} 5 6public void setSummary(HtmlPage currentPage, String summary) { 7 HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); 8 summaryInput.setValueAttribute(summary); 9}
1fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{ 2 setSummary(currentPage, summary); 3 // ... 4} 5 6fun setSummary(currentPage:HtmlPage , summary: String) { 7 val summaryInput = currentPage.getHtmlElementById("summary") 8summaryInput.setValueAttribute(summary) 9}
이렇게 하면 UI를 변경하더라도 모든 테스트를 업데이트할 필요 없습니다.
한 걸음 더 나아가, 다음 예제가 보여 주듯이 현재 위치한 HtmlPage를 나타내는 Object 내에 이 로직을 배치할 수도 있습니다:
1public class CreateMessagePage { 2 3 final HtmlPage currentPage; 4 5 final HtmlTextInput summaryInput; 6 7 final HtmlSubmitInput submit; 8 9 public CreateMessagePage(HtmlPage currentPage) { 10 this.currentPage = currentPage; 11 this.summaryInput = currentPage.getHtmlElementById("summary"); 12 this.submit = currentPage.getHtmlElementById("submit"); 13 } 14 15 public <T> T createMessage(String summary, String text) throws Exception { 16 setSummary(summary); 17 18 HtmlPage result = submit.click(); 19 boolean error = CreateMessagePage.at(result); 20 21 return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result)); 22 } 23 24 public void setSummary(String summary) throws Exception { 25 summaryInput.setValueAttribute(summary); 26 } 27 28 public static boolean at(HtmlPage page) { 29 return "Create Message".equals(page.getTitleText()); 30 } 31}
1class CreateMessagePage(private val currentPage: HtmlPage) { 2 3 val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary") 4 5 val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit") 6 7 fun <T> createMessage(summary: String, text: String): T { 8 setSummary(summary) 9 10 val result = submit.click() 11 val error = at(result) 12 13 return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T 14 } 15 16 fun setSummary(summary: String) { 17 summaryInput.setValueAttribute(summary) 18 } 19 20 fun at(page: HtmlPage): Boolean { 21 return "Create Message" == page.getTitleText() 22 } 23}
이전에는 이 패턴을 Page Object Pattern이라고 불렀습니다. HtmlUnit으로도 물론 이 작업을 수행할 수 있지만, WebDriver는 다음 섹션에서 살펴볼 몇 가지 도구를 제공하여 이 패턴을 훨씬 더 쉽게 구현할 수 있게 해 줍니다.
Selenium WebDriver를 MockMvc와 함께 사용하려면, 프로젝트에
org.seleniumhq.selenium:htmlunit3-driver에 대한 테스트 의존성이 포함되어 있는지 확인해야 합니다.
다음 예제가 보여 주듯이, MockMvcHtmlUnitDriverBuilder를 사용하여 MockMvc와 통합되는 Selenium WebDriver를 쉽게 생성할 수 있습니다:
1WebDriver driver; 2 3@BeforeEach 4void setup(WebApplicationContext context) { 5 driver = MockMvcHtmlUnitDriverBuilder 6 .webAppContextSetup(context) 7 .build(); 8}
1lateinit var driver: WebDriver 2 3@BeforeEach 4fun setup(context: WebApplicationContext) { 5 driver = MockMvcHtmlUnitDriverBuilder 6 .webAppContextSetup(context) 7 .build() 8}
이것은 MockMvcHtmlUnitDriverBuilder를 사용하는 간단한 예제입니다. 더 고급 사용법은<br>Advanced MockMvcHtmlUnitDriverBuilder를 참조하십시오. |
앞의 예제는 서버로 localhost를 참조하는 모든 URL이 실제 HTTP 연결 없이
MockMvc 인스턴스로 전달되도록 보장합니다. 그 외의 URL은 일반적인 방식대로 네트워크 연결을 사용하여 요청됩니다.
이를 통해 CDN 사용을 손쉽게 테스트할 수 있습니다.
이제 애플리케이션을 서블릿 컨테이너에 배포할 필요 없이 평소처럼 WebDriver를 사용할 수 있습니다. 예를 들어, 다음과 같이 메시지를 생성하는 뷰를 요청할 수 있습니다:
1CreateMessagePage page = CreateMessagePage.to(driver);
1val page = CreateMessagePage.to(driver)
그런 다음 다음과 같이 폼을 채우고 제출하여 메시지를 생성할 수 있습니다:
1ViewMessagePage viewMessagePage = 2 page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
1val viewMessagePage = 2 page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)
이는 Page Object Pattern을 활용함으로써
HtmlUnit 테스트의 설계를 개선합니다.
Why WebDriver and MockMvc?에서 언급했듯이,
HtmlUnit으로도 Page Object Pattern을 사용할 수 있지만 WebDriver를 사용하면 훨씬 더 쉽습니다.
다음 CreateMessagePage 구현을 살펴보십시오:
1public class CreateMessagePage extends AbstractPage { (1) 2 3 (2) 4 private WebElement summary; 5 private WebElement text; 6 7 @FindBy(css = "input[type=submit]") (3) 8 private WebElement submit; 9 10 public CreateMessagePage(WebDriver driver) { 11 super(driver); 12 } 13 14 public <T> T createMessage(Class<T> resultPage, String summary, String details) { 15 this.summary.sendKeys(summary); 16 this.text.sendKeys(details); 17 this.submit.click(); 18 return PageFactory.initElements(driver, resultPage); 19 } 20 21 public static CreateMessagePage to(WebDriver driver) { 22 driver.get("http://localhost:9990/mail/messages/form"); 23 return PageFactory.initElements(driver, CreateMessagePage.class); 24 } 25}
| 1 | CreateMessagePage는 AbstractPage를 확장합니다. AbstractPage의 세부 내용은 다루지 않지만,<br>요약하자면 모든 페이지에 대한 공통 기능을 포함합니다. 예를 들어, 애플리케이션에 내비게이션 바, 전역 에러 메시지,<br>기타 기능이 있다면 이 로직을 공유 위치에 둘 수 있습니다. |
| 2 | HTML 페이지에서 우리가 관심 있는 각 부분에 대해 멤버 변수를 가집니다. 이들은 WebElement 타입입니다.<br>WebDriver의<br>PageFactory는 각 WebElement를 자동으로 해석함으로써<br>HtmlUnit 버전의 CreateMessagePage에서 많은 코드를 제거할 수 있게 해 줍니다.<br>PageFactory#initElements(WebDriver,Class<T>)<br>메서드는 필드 이름을 사용하여 HTML 페이지 내 요소의 id 또는 name으로 조회함으로써 각 WebElement를 자동으로 해석합니다. |
| 3 | 기본 조회 동작을 오버라이드하기 위해<br>@FindBy 어노테이션을 사용할 수 있습니다.<br>예제에서는 @FindBy 어노테이션을 사용하여 css 셀렉터 (input[type=submit])로 제출 버튼을 조회하는 방법을 보여 줍니다. |
1class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1) 2 3 (2) 4 private lateinit var summary: WebElement 5 private lateinit var text: WebElement 6 7 @FindBy(css = "input[type=submit]") (3) 8 private lateinit var submit: WebElement 9 10 fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T { 11 this.summary.sendKeys(summary) 12 text.sendKeys(details) 13 submit.click() 14 return PageFactory.initElements(driver, resultPage) 15 } 16 17 companion object { 18 fun to(driver: WebDriver): CreateMessagePage { 19 driver.get("http://localhost:9990/mail/messages/form") 20 return PageFactory.initElements(driver, CreateMessagePage::class.java) 21 } 22 } 23}
| 1 | CreateMessagePage는 AbstractPage를 확장합니다. AbstractPage의 세부 내용은 다루지 않지만,<br>요약하자면 모든 페이지에 대한 공통 기능을 포함합니다. 예를 들어, 애플리케이션에 내비게이션 바, 전역 에러 메시지,<br>기타 기능이 있다면 이 로직을 공유 위치에 둘 수 있습니다. |
| 2 | HTML 페이지에서 우리가 관심 있는 각 부분에 대해 멤버 변수를 가집니다. 이들은 WebElement 타입입니다.<br>WebDriver의<br>PageFactory는 각 WebElement를 자동으로 해석함으로써<br>HtmlUnit 버전의 CreateMessagePage에서 많은 코드를 제거할 수 있게 해 줍니다.<br>PageFactory#initElements(WebDriver,Class<T>)<br>메서드는 필드 이름을 사용하여 HTML 페이지 내 요소의 id 또는 name으로 조회함으로써 각 WebElement를 자동으로 해석합니다. |
| 3 | 기본 조회 동작을 오버라이드하기 위해<br>@FindBy 어노테이션을 사용할 수 있습니다.<br>예제에서는 @FindBy 어노테이션을 사용하여 css 셀렉터 ( input[type=submit])로 제출 버튼을 조회하는 방법을 보여 줍니다. |
마지막으로, 새 메시지가 성공적으로 생성되었는지 확인할 수 있습니다. 다음 어서션은 AssertJ 어서션 라이브러리를 사용합니다:
1assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage); 2assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
1assertThat(viewMessagePage.message).isEqualTo(expectedMessage) 2assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")
ViewMessagePage를 통해 커스텀 도메인 모델과 상호 작용할 수 있음을 알 수 있습니다.
예를 들어, Message 객체를 반환하는 메서드를 노출합니다:
1public Message getMessage() throws ParseException { 2 Message message = new Message(); 3 message.setId(getId()); 4 message.setCreated(getCreated()); 5 message.setSummary(getSummary()); 6 message.setText(getText()); 7 return message; 8}
1fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())
그런 다음 어서션에서 리치 도메인 객체를 사용할 수 있습니다.
마지막으로, 테스트가 완료되면 WebDriver 인스턴스를 닫는 것을 잊지 말아야 합니다:
1@AfterEach 2void destroy() { 3 if (driver != null) { 4 driver.close(); 5 } 6}
1@AfterEach 2fun destroy() { 3 if (driver != null) { 4 driver.close() 5 } 6}
WebDriver 사용에 대한 추가 정보는 Selenium WebDriver 문서를 참조하십시오.
MockMvcHtmlUnitDriverBuilder지금까지의 예제에서는 Spring TestContext Framework가 로드해 주는
WebApplicationContext를 기반으로 WebDriver를 빌드하는 가장 단순한 방식으로
MockMvcHtmlUnitDriverBuilder를 사용했습니다. 이 접근 방식은 다음과 같이 다시 나타납니다:
1WebDriver driver; 2 3@BeforeEach 4void setup(WebApplicationContext context) { 5 driver = MockMvcHtmlUnitDriverBuilder 6 .webAppContextSetup(context) 7 .build(); 8}
1lateinit var driver: WebDriver 2 3@BeforeEach 4fun setup(context: WebApplicationContext) { 5 driver = MockMvcHtmlUnitDriverBuilder 6 .webAppContextSetup(context) 7 .build() 8}
다음과 같이 추가 설정 옵션을 지정할 수도 있습니다:
1WebDriver driver; 2 3@BeforeEach 4void setup() { 5 driver = MockMvcHtmlUnitDriverBuilder 6 // demonstrates applying a MockMvcConfigurer (Spring Security) 7 .webAppContextSetup(context, springSecurity()) 8 // for illustration only - defaults to "" 9 .contextPath("") 10 // By default MockMvc is used for localhost only; 11 // the following will use MockMvc for example.com and example.org as well 12 .useMockMvcForHosts("example.com","example.org") 13 .build(); 14}
1lateinit var driver: WebDriver 2 3@BeforeEach 4fun setup() { 5 driver = MockMvcHtmlUnitDriverBuilder 6 // demonstrates applying a MockMvcConfigurer (Spring Security) 7 .webAppContextSetup(context, springSecurity()) 8 // for illustration only - defaults to "" 9 .contextPath("") 10 // By default MockMvc is used for localhost only; 11 // the following will use MockMvc for example.com and example.org as well 12 .useMockMvcForHosts("example.com","example.org") 13 .build() 14}
대안으로, 다음과 같이 MockMvc 인스턴스를 별도로 구성하고 이를 MockMvcHtmlUnitDriverBuilder에 제공하여
정확히 동일한 설정을 수행할 수 있습니다:
1MockMvc mockMvc = MockMvcBuilders 2 .webAppContextSetup(context) 3 .apply(springSecurity()) 4 .build(); 5 6driver = MockMvcHtmlUnitDriverBuilder 7 .mockMvcSetup(mockMvc) 8 // for illustration only - defaults to "" 9 .contextPath("") 10 // By default MockMvc is used for localhost only; 11 // the following will use MockMvc for example.com and example.org as well 12 .useMockMvcForHosts("example.com","example.org") 13 .build();
1val mockMvc: MockMvc = MockMvcBuilders 2 .webAppContextSetup(context) 3 .apply<DefaultMockMvcBuilder>(springSecurity()) 4 .build() 5 6driver = MockMvcHtmlUnitDriverBuilder 7 .mockMvcSetup(mockMvc) 8 // for illustration only - defaults to "" 9 .contextPath("") 10 // By default MockMvc is used for localhost only; 11 // the following will use MockMvc for example.com and example.org as well 12 .useMockMvcForHosts("example.com", "example.org") 13 .build()
이 방식은 더 장황하지만, MockMvc 인스턴스로 WebDriver를 빌드함으로써
MockMvc의 모든 기능을 활용할 수 있습니다.
MockMvc 인스턴스 생성에 대한 추가 정보는<br>Configuring MockMvc를 참조하십시오. |
MockMvc and HtmlUnit
MockMvc and Geb