단축 URL에 사용할 프론트 페이지를 만들었다! 폼 코드는 아래와 같다.
<form id="createForm">
<label for="originalUrl">원본 URL : </label>
<input type="text"
id="originalUrl"
name="originalUrl"
placeholder="http://example.com">
<button type="submit">생성</button>
</form>
<p id="generatedShortenUrl"></p>
originalUrl
name으로 input을 받을 것이다. 생성 버튼을 눌러 submit 이벤트가 발생하면, 지정한 url로 POST 요청을 하도록 구성했다. 동일한 페이지의 scrpit 태그에 해당 역할을 수행하는 javascript 코드를 작성했다.
코드가 좀 길다 ㅜㅜ fetch
API를 사용했다. 예시론 URL 생성 폼만 작성했지만 조회 폼도 전반적인 기능은 같다! method와 BODY 존재 유무만 다를 뿐..
document.addEventListener('DOMContentLoaded', function() {
const createForm = document.getElementById('createForm');
createForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(createForm);
var requestData = {};
formData.forEach(function(value, key) {
requestData[key] = value;
});
fetch('/url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
})
.then(function(response) {
if (response.ok) {
return response.json();
} else {
return response.json().then(function(err) {
throw new Error(err.message);
});
}
})
.then(function(data) {
const generatedDom = document.getElementById('generatedShortenUrl');
generatedDom.innerHTML = "생성된 단축 URL : " + data.shortenUrl;
})
.catch(function(error) {
const generatedDom = document.getElementById('generatedShortenUrl');
generatedDom.innerHTML = "잘못된 URL입니다.";
});
});
});
}
createForm
DOM에 addEventListener
을 사용해 제출 이벤트를 핸들링하고 있다.requestData
에 폼으로 제출된 originalUrl
데이터를 담고, fetch
API를 통해 /url
path로 POST 요청을 날렸다.generatedShortenUrl
를 가진 p태그에 응답으로 온 data
의 shortenUrl
값이 들어가게 되고, 그렇지 않으면 "잘못된 URL입니다"라는 값이 들어가게 구성했다.원본 URL input 항목에 https://youtube.com
을 입력하고 "생성"버튼을 클릭했다.
생성된 단축 URL이 뜬다. 이제
localhost:8080/g8mixKb
에 접속해보면?
굳굳! (근데 유튜브 메인 너무씹덕아니에요?)
이제 단축 URL 조회 폼에 방금의 단축된 URL을 기입해보았다.
요청 횟수도 잘 표시된다!
유효하지 않은 URL을 등록하려 하거나, 존재하지 않는 URL을 조회하려고 하면 오류 문구가 나온다.
MockMvc
와 @MockBean
을 이용해서 컨트롤러 테스트를 한다.
test 디렉토리 아래, controller 패키지에 다음과 같은 Test 클래스를 추가해줬다. @ExtendWith
을 통해 mockito를 이용한 단위 테스트를 할 것임을 명시했고, mockMvc를 통해 가짜 request를 날릴 준비를 마쳤다. ObjectMapper
은 요청으로 날릴 body객체를 위함이고, UrlService
서비스 객체를 가짜 객체로 만들어줄 @MockBean
어노테이션을 사용했다.
@WebMvcTest(UrlRestController.class)
@ExtendWith(MockitoExtension.class)
public class UrlRestControllerTest {
@MockBean
private UrlService urlService;
@Autowired // 자동으로 mockMvc 빌드
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
}
일단 valid한 POST 요청에 대해, 단축 URL 생성이 잘 된 응답이 도착하는지 확인하는 코드이다.
@DisplayName("단축 URL 생성 확인")
@Test
public void createShortenUrl() throws Exception {
Mockito.when(urlService.shortenUrl(Mockito.anyString()))
.thenReturn(ShortenUrl.builder()
.originalUrl("http://test.com")
.shortenUrl("test123")
.build());
mockMvc.perform(
MockMvcRequestBuilders
.post("/url")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(
new CreateShortenUrlDto.Request(Mockito.anyString()))))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.shortenUrl").exists());
}
when().thenReturn()
메소드를 통해 가짜 객체 urlService
의 동작을 지정했다. 서비스 빈의 shortenUrl()
이 호출되면 특정 ShortenUrl
도메인 객체가 반환될 것이다.MockMvcRequestBuilders
를 통해 POST 요청을 날렸다. body로 지정된 json 값을 줬을 때, 1) 응답 코드가 200인지, 2) 단축된 URL 결과물 항목인 shortenUrl
이 존재하는지 andExpect()
를 통해 확인했다.위대하신 멘토님 가로되, 테스트에서 정말 중요한 것은 라이브러리의 사용법 그 자체보다 유닛 테스트를 하고자 하는 클래스의 어떤 기능을 테스트 할 것인지, 그러기 위해서 mock으로 어떤 객체를 두고 그 객체의 mocking 행동을 어떻게 구성할 것인지 고민하는 것이 더 중요하다고 .. 맞는 말씀이다.
실제 단축된 URL로 리다이렉트가 이뤄지는지, 즉 상태코드가 300번대이고 헤더의 Location 항목이 존재하는지 역시 테스트해보았다.
@DisplayName("리다이렉트 테스트")
@Test
public void createWithInvalidRequest() throws Exception {
Mockito.when(urlService.getOriginalUrl(Mockito.anyString()))
.thenReturn("http://test.com");
mockMvc.perform(
MockMvcRequestBuilders.get("/{shortenUrl}","test123"))
.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
.andExpect(MockMvcResultMatchers.header().exists("Location"))
.andDo(MockMvcResultHandlers.print());
}
urlService
의 getOriginalUrl()
이 호출되었을 때 반환할 가짜 원본 url을 동작시켰다.andExpect()
를 통해 리다이렉트 동작을 위한 상태코드와 헤더 항목이 존재함을 확인하고,andDo()
를 통해 HTTP 메세지 항목들을 테스트하며 확인할 수 있도록 했다.역시 서비스 테스트를 위해 기본 셋팅을 했다.
@ExtendWith(MockitoExtension.class)
public class UrlServiceUnitTest {
@InjectMocks
private UrlService urlService;
@Mock
private UrlRepository urlRepository;
}
@InjectMocks
를 통해 urlService
빈에 mock 객체들을 주입할 것이라고 표지했다.urlRepository
는 서비스 객체에 주입될 mock repository이다.서비스 유닛 테스트에선 서비스 로직이 잘 동작하는지 확인한다!
@DisplayName("형식에 맞지 않는 URL 요청일 경우 InvalidUrlException")
@Test
public void requestWithInvalidUrl() {
String invalidUrl = "test.invalid";
Assertions.assertThrows(InvalidUrlException.class,() -> {
urlService.shortenUrl(invalidUrl);
});
}
@DisplayName("존재하지 않는 URL 조회 요청일 경우 UrlNotFoundException")
@Test
public void requestWithNotExistUrl() {
Mockito.when(urlRepository.findByShortenUrl(Mockito.anyString()))
.thenReturn(Optional.empty());
Assertions.assertThrows(UrlNotFoundException.class,() -> {
urlService.getByShortenUrl("http://test.com");
});
}
여기선 요청된 URL에 대한 fail validation과 repository에서 null을 반환했을 때 예외가 잘 던져지는지에 대해 테스트했다.
마지막으로 domain 영역의 기능까지 이용하는 통합 테스트를 했다.
@SpringBootTest
public class UrlServiceTest {
@Autowired
private UrlService urlService;
@DisplayName("단축된 URL이 7자리가 맞는지 확인")
@Test
public void shortenUrlLengthTest() {
String testUrl = "http://test.com";
Assertions.assertEquals(7,
urlService.shortenUrl(testUrl).getShortenUrl().length());
}
@DisplayName("저장한 shortenUrl을 다시 불러왔을 때 같은지 확인")
@Test
public void testSavedUrl() {
String testUrl = "http://test.com";
String savedShortenUrl = urlService.shortenUrl(testUrl).getShortenUrl();
Assertions.assertEquals(testUrl, urlService.getOriginalUrl(savedShortenUrl));
}
}
urlService.sortenUrl()
에서 URL을 생성하는 로직, 생성된 URL을 DB에 저장하는 로직까지 한꺼번에 수행하고 있어서 통합 테스트로만 확인이 가능했따 ~_~... 더 좋은 방법이 있었을까?