Spring에서 HTML 결과 테스트하기 (feat. RestAssured)
서론
RestAssured
를 통한 Spring 테스트
구현 시,
반환되는 HTML
내부에 특정 데이터가 들어있는지 확인할 필요가 생겼습니다.
SSR(Server-Side Rendering)
방식으로 Controller
에서 HTML
로 쓰여진 View
를 반환하는 경우,
데이터가 HTML
내부로 렌더링된 후 반환됩니다.
따라서 동작을 제대로 검증하려면, HTML
내부에 의도한 값이 들어있는지를 검증할 필요가 있습니다.
RestAssured로 HTML 검증하기
HTML 꺼내기
우선 원하는 응답을 저장해야 합니다.
방법 1)
Response response = get("/");
방법 2)
ExtractableResponse<Response> extractableResponse = get("/")
.then().log().all()
.extract();
Response
는 기본적인 형태의 응답을 저장합니다.
ExtractableResponse
는 extract()
메서드로 반환한 결과를 담고 있습니다.
JSON
형식으로 추출하는 등의 추가 기능이 가능합니다.
get()
메서드 이후 체이닝으로 log()
를 붙이는 경우, Response
로 반환이 되지 않아
extract()
를 통해 ExtractableResponse
로 응답을 저장해보겠습니다.
String xmlString = response.asString();
XmlPath xmlPath = new XmlPath(CompatibilityMode.HTML, xmlString);
HTML
정보가 담긴 XmlPath
인스턴스가 생성되었습니다.
HTML의 원하는 정보 꺼내기 1
위에서 get(”/”)
요청에 대해 log()
를 호출했는데요,
그 결과는 아래와 같습니다.
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>상품목록</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/css/base.css"/>
<link rel="stylesheet" href="/css/styles.css"/>
</head>
<body>
<header class="gnb">
<nav>
<ul class="gnb-group">
<li>
<a shape="rect" href="/">상품목록</a>
</li>
<li>
<a shape="rect" href="/cart">장바구니</a>
</li>
<li>
<a shape="rect" href="/settings">설정</a>
</li>
<li class="nav-admin">
<a shape="rect" href="/admin">관리자</a>
</li>
</ul>
</nav>
</header>
<div class="container">
<ul class="product-grid">
<li class="product">
<div class="product-image">
<img alt="상품 이미지" src="super.com"/>
</div>
<div class="product-info">
<div class="product-desc">
<p class="product-name">사과즙</p>
<p class="product-price">1000</p>
</div>
<button type="submit" class="product-btn" onclick="addCartItem(1)">담기</button>
</div>
</li>
<li class="product">
<div class="product-image">
<img alt="상품 이미지" src="super.com"/>
</div>
<div class="product-info">
<div class="product-desc">
<p class="product-name">포도</p>
<p class="product-price">2000</p>
</div>
<button type="submit" class="product-btn" onclick="addCartItem(3)">담기</button>
</div>
</li>
</ul>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"/>
<script src="/js/cart.js"/>
</body>
</html>
위의 HTML
에서 포도
와 2000
이라는 값을 꺼내보겠습니다.
System.out.println(xmlPath2.getString(
"html.body.div" // 1번 문장
+ ".ul.find { it.@class == 'product-grid' }"
+ ".li[1]" // 2번 문장
+ ".div.find { it.@class == 'product-info' }" // 3번 문장
+ ".div.find { it.@class == 'product-desc' }" // 4번 문장
+ ".p.find { it.@class == 'product-name' }"));
// "포도" 출력
1번 문장)
가장 상위부터 원하는 값이 있는 곳까지 블럭을 꺼내면 됩니다.
→ html.body.div.ul ...
1번 문장)
해당 위치에 같은 블럭이 한 개 있는 경우 바로 꺼낼 수 있습니다. (여러 개라면 0번째가 꺼내집니다.)
→ .div.find { it.@class == 'container' }
2번 문장)
해당 위치에 같은 블럭이 여러 개 있는데, 이름이 모두 같은 경우 인덱싱으로 꺼낼 수 있습니다.
→ .li[1]
3, 4번 문장)
해당 위치에 같은 블럭(div)이 여러 개 있지만 이름이 다른 경우 → 이름으로 꺼낼 수 있습니다.
→ .div.find { it.@class == 'product-info' }
→ .div.find { it.@class == 'product-desc' }
포도의 가격에 해당하는 2000
은 아래와 같이 꺼낼 수 있습니다.
System.out.println(xmlPath2.getString(
"html.body.div"
+ ".ul.find { it.@class == 'product-grid' }"
+ ".li[1]"
+ ".div.find { it.@class == 'product-info' }"
+ ".div.find { it.@class == 'product-desc' }"
+ ".p.find { it.@class == 'product-price' }")); // 여기만 수정
// "2000" 출력
HTML의 원하는 정보 꺼내기 2 - table 정보
이번에는 아래처럼 table
정보가 있는 HTML
에서 원하는 값을 찾아보겠습니다.
원리는 동일합니다.
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>관리자 페이지</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/css/base.css"/>
<link rel="stylesheet" href="/css/admin.css"/>
</head>
<body>
<header class="gnb">
<nav>
<ul class="gnb-group">
<li>
<a shape="rect" href="/">상품목록</a>
</li>
<li>
<a shape="rect" href="/cart">장바구니</a>
</li>
<li>
<a shape="rect" href="/settings">설정</a>
</li>
<li class="nav-admin">
<a shape="rect" href="/admin">관리자</a>
</li>
</ul>
</nav>
</header>
<div class="container">
<div class="btn-group">
<button type="submit" class="add-btn" onclick="showAddModal()">상품 추가</button>
</div>
<table>
<tr>
<th colspan="1" rowspan="1">ID</th>
<th colspan="1" rowspan="1">이름</th>
<th colspan="1" rowspan="1">가격</th>
<th colspan="1" rowspan="1">이미지</th>
<th colspan="1" rowspan="1">Actions</th>
</tr>
<tbody id="product-list">
<tr>
<td colspan="1" rowspan="1">1</td>
<td colspan="1" rowspan="1">사과즙</td>
<td colspan="1" rowspan="1">1000</td>
<td colspan="1" rowspan="1">
<img style="max-width: 100px;" src="super.com"/>
</td>
<td colspan="1" rowspan="1">
<button type="submit" onclick="showEditModal({"id":1,"name":"\uC0AC\uACFC\uC999","price":1000,"imageUrl":"super.com"})">수정</button>
<button type="submit" onclick="deleteProduct(1)">삭제</button>
</td>
</tr>
<tr>
<td colspan="1" rowspan="1">3</td>
<td colspan="1" rowspan="1">포도</td>
<td colspan="1" rowspan="1">2000</td>
<td colspan="1" rowspan="1">
<img style="max-width: 100px;" src="super.com"/>
</td>
<td colspan="1" rowspan="1">
<button type="submit" onclick="showEditModal({"id":3,"name":"\uD3EC\uB3C4","price":2000,"imageUrl":"super.com"})">수정</button>
<button type="submit" onclick="deleteProduct(3)">삭제</button>
</td>
</tr>
</tbody>
</table>
<div class="modal" id="modal" data-form-type="add">
<div class="modal-content">
<span class="close" onclick="hideAddModal()">×</span>
<form enctype="application/x-www-form-urlencoded" method="get" id="form">
<label for="name">상품명</label>
<br clear="none"/>
<input type="text" id="name" name="name" required="required"/>
<br clear="none"/>
<label for="price">가격</label>
<br clear="none"/>
<input type="number" id="price" name="price" required="required"/>
<br clear="none"/>
<label for="image-url">이미지 URL</label>
<br clear="none"/>
<input type="text" id="image-url" name="imageUrl" required="required"/>
<br clear="none"/>
<button type="submit">제출</button>
</form>
</div>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"/>
<script src="/js/admin.js"/>
</body>
</html>
위의 HTML
응답에서, 마찬가지로 포도
값을 꺼내보겠습니다.
System.out.println(xmlPath.getString("html.body.div.table.tbody.tr[1].td[1]"));
// "포도" 출력
이름으로 구분할 필요가 없고, 순서가 명확해서 인덱싱으로만 값을 꺼낼 수 있었습니다.
이렇게 꺼낸 값들을, 아래와 같이 assertion
을 통해 테스트할 수 있습니다.
assertAll(
() -> assertThat(
xmlPath.getString("html.body.div.table.tbody.tr[0].td[1]")
).contains("사과즙"),
() -> assertThat(
xmlPath.getString("html.body.div.table.tbody.tr[0].td[2]")
).contains("1000"),
() -> assertThat(
xmlPath.getString("html.body.div.table.tbody.tr[1].td[1]")
).contains("포도")
);
View에 지나치게 의존적이다
위의 테스트 방식은,
화면(HTML
)의 구조가 바뀌는 순간 테스트가 실패할 수 있습니다.
그리고 그 수정이 무척 까다롭습니다.
따라서, 적당히 절충하여 아래와 같이 테스트할 수 있습니다.
// 응답 추출
String responseString extractableResponse = get("/")
.then().log().all()
.extract()
.asString();
assertThat(responseString).contains("포도");
중복되지 않는 데이터를 테스트한다면,
위와 같은 테스트만으로 동작이 제대로 이루어졌는지 판단할 수 있을 것입니다.
물론 정확한 위치까지 테스트하는 것이 필요할 경우도 있겠습니다.
더 좋은 방식이 있다면 추천해주세요!
감사합니다.
참고자료
- RestAssured docs (Github) : https://github.com/rest-assured/rest-assured/wiki/Usage#xml-using-xmlpath
- Javadoc : https://www.javadoc.io/doc/io.rest-assured/xml-path/latest/io/restassured/path/xml/XmlPath.html