Java/Spring, Spring Boot

Spring에서 HTML 결과 테스트하기 (feat. RestAssured)

열심히 사는 우진 2023. 5. 1. 09:52
반응형

서론

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는 기본적인 형태의 응답을 저장합니다.

ExtractableResponseextract() 메서드로 반환한 결과를 담고 있습니다.

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&amp;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&amp;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({&quot;id&quot;:1,&quot;name&quot;:&quot;\uC0AC\uACFC\uC999&quot;,&quot;price&quot;:1000,&quot;imageUrl&quot;:&quot;super.com&quot;})">수정</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({&quot;id&quot;:3,&quot;name&quot;:&quot;\uD3EC\uB3C4&quot;,&quot;price&quot;:2000,&quot;imageUrl&quot;:&quot;super.com&quot;})">수정</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("포도");

중복되지 않는 데이터를 테스트한다면,

위와 같은 테스트만으로 동작이 제대로 이루어졌는지 판단할 수 있을 것입니다.

 

물론 정확한 위치까지 테스트하는 것이 필요할 경우도 있겠습니다.

 


 

더 좋은 방식이 있다면 추천해주세요!

감사합니다.

 

 

참고자료

 

 

 

 

반응형