fix(i18n): 파파고 번역 요청을 길이 제한에 맞춰 분할한다
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
||||
package kr.co.vividnext.sodalive.i18n.translation
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.util.ReflectionTestUtils
|
||||
import org.springframework.test.web.client.MockRestServiceServer
|
||||
import org.springframework.test.web.client.ResponseActions
|
||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.content
|
||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.header
|
||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.method
|
||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
|
||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
|
||||
import org.springframework.web.client.RestTemplate
|
||||
|
||||
@SpringBootTest(
|
||||
classes = [PapagoTranslationService::class],
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE,
|
||||
properties = [
|
||||
"cloud.naver.papago-client-id=test-client-id",
|
||||
"cloud.naver.papago-client-secret=test-client-secret"
|
||||
]
|
||||
)
|
||||
class PapagoTranslationServiceTest @Autowired constructor(
|
||||
private val service: PapagoTranslationService
|
||||
) {
|
||||
private val objectMapper = ObjectMapper()
|
||||
private lateinit var server: MockRestServiceServer
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
val restTemplate = RestTemplate()
|
||||
server = MockRestServiceServer.bindTo(restTemplate).build()
|
||||
ReflectionTestUtils.setField(service, "restTemplate", restTemplate)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun verifyRequests() {
|
||||
server.verify()
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = [12, 5000])
|
||||
@DisplayName("5,000자 이하 원문은 기존 형식으로 한 번만 요청한다")
|
||||
fun shouldTranslateTextWithinLimitInOneRequest(length: Int) {
|
||||
val source = "a".repeat(length)
|
||||
expectTranslation(source, "translated")
|
||||
|
||||
assertEquals(listOf("translated"), translate(source).translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5,001자 원문은 두 번 요청하고 번역을 구분자 없이 결합한다")
|
||||
fun shouldSplit5001UnitsAndJoinTranslationsWithoutSeparator() {
|
||||
val first = "a".repeat(5000)
|
||||
expectTranslation(first, "first")
|
||||
expectTranslation("b", "second")
|
||||
|
||||
assertEquals(listOf("firstsecond"), translate(first + "b").translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10,001자 원문은 순서대로 세 번 요청하고 결과 하나를 반환한다")
|
||||
fun shouldSplit10001UnitsInSourceOrder() {
|
||||
val first = "a".repeat(5000)
|
||||
val second = "b".repeat(5000)
|
||||
expectTranslation(first, "first")
|
||||
expectTranslation(second, "second")
|
||||
expectTranslation("c", "third")
|
||||
|
||||
assertEquals(listOf("firstsecondthird"), translate(first + second + "c").translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("한글 원문의 줄바꿈과 공백을 분할 요청에 그대로 보존한다")
|
||||
fun shouldPreserveKoreanTextAndWhitespaceAcrossChunks() {
|
||||
val first = "가".repeat(4999) + "\n"
|
||||
expectTranslation(first, "translated ")
|
||||
expectTranslation(" ", "tail")
|
||||
|
||||
assertEquals(listOf("translated tail"), translate(first + " ").translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5,000자 경계의 서로게이트 쌍은 다음 조각에 함께 포함한다")
|
||||
fun shouldKeepSurrogatePairTogetherAtChunkBoundary() {
|
||||
val first = "a".repeat(4999)
|
||||
val second = "\uD83D\uDE00b"
|
||||
expectTranslation(first, "first")
|
||||
expectTranslation(second, "second")
|
||||
|
||||
assertEquals(listOf("firstsecond"), translate(first + second).translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("여러 원문의 조각을 순차 요청하고 원문별 결과 대응을 유지한다")
|
||||
fun shouldReturnOneJoinedTranslationPerOriginalSource() {
|
||||
val first = "a".repeat(5000)
|
||||
val second = "b".repeat(5000)
|
||||
expectTranslation(first, "first-")
|
||||
expectTranslation("a", "tail")
|
||||
expectTranslation(second, "second-")
|
||||
expectTranslation("b", "tail")
|
||||
|
||||
assertEquals(listOf("first-tail", "second-tail"), translate(first + "a", second + "b").translatedText)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = [400, 500])
|
||||
@DisplayName("중간 조각의 HTTP 오류는 전체 결과를 버리고 이후 조각과 원문을 요청하지 않는다")
|
||||
fun shouldAbortAllTranslationsAfterHttpErrorInMiddleChunk(status: Int) {
|
||||
val first = "a".repeat(5000)
|
||||
val second = "b".repeat(5000)
|
||||
expectTranslation("earlier", "earlier translation")
|
||||
expectTranslation(first, "partial")
|
||||
expectRequest(second).andRespond(withStatus(HttpStatus.valueOf(status)))
|
||||
|
||||
assertEquals(emptyList<String>(), translate("earlier", first + second + "c", "later").translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("다음 원문의 HTTP 오류도 앞선 성공 결과를 버리고 후속 요청을 중단한다")
|
||||
fun shouldDiscardEarlierSourcesAndStopAfterHttpFailure() {
|
||||
expectTranslation("earlier", "earlier translation")
|
||||
expectRequest("broken").andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
|
||||
|
||||
assertEquals(emptyList<String>(), translate("earlier", "broken", "later").translatedText)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["", " \t\n"])
|
||||
@DisplayName("중간 조각의 번역이 비었거나 공백이면 전체 결과를 버리고 후속 요청을 중단한다")
|
||||
fun shouldAbortAllTranslationsAfterBlankMiddleChunk(translated: String) {
|
||||
val first = "a".repeat(5000)
|
||||
val second = "b".repeat(5000)
|
||||
expectTranslation("earlier", "earlier translation")
|
||||
expectTranslation(first, "partial")
|
||||
expectTranslation(second, translated)
|
||||
|
||||
assertEquals(emptyList<String>(), translate("earlier", first + second + "c", "later").translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("응답 본문이 없으면 앞선 성공 결과를 포함하지 않는 빈 결과를 반환한다")
|
||||
fun shouldDiscardAllTranslationsWhenResponseBodyIsMissing() {
|
||||
expectTranslation("earlier", "earlier translation")
|
||||
expectRequest("empty").andRespond(withSuccess("", MediaType.APPLICATION_JSON))
|
||||
|
||||
assertEquals(emptyList<String>(), translate("earlier", "empty").translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("빈 입력 목록은 HTTP 요청 없이 빈 결과를 반환한다")
|
||||
fun shouldNotRequestTranslationForEmptySources() {
|
||||
assertEquals(emptyList<String>(), translate().translatedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일한 출발 언어와 도착 언어는 HTTP 요청 없이 빈 결과를 반환한다")
|
||||
fun shouldNotRequestTranslationForSameLanguage() {
|
||||
val result = service.translate(TranslateRequest(listOf("source"), "ko", "ko"))
|
||||
|
||||
assertEquals(emptyList<String>(), result.translatedText)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource("fr,en", "ko,fr")
|
||||
@DisplayName("미지원 출발 언어 또는 도착 언어는 HTTP 요청 없이 빈 결과를 반환한다")
|
||||
fun shouldNotRequestTranslationForUnsupportedLanguage(sourceLanguage: String, targetLanguage: String) {
|
||||
val result = service.translate(TranslateRequest(listOf("source"), sourceLanguage, targetLanguage))
|
||||
|
||||
assertEquals(emptyList<String>(), result.translatedText)
|
||||
}
|
||||
|
||||
private fun translate(vararg texts: String): TranslateResult {
|
||||
return service.translate(TranslateRequest(texts.toList(), "ko", "en"))
|
||||
}
|
||||
|
||||
private fun expectTranslation(source: String, translated: String) {
|
||||
val response = PapagoTranslationResponse(
|
||||
PapagoTranslationResponse.Message(PapagoTranslationResponse.Result("ko", "en", translated))
|
||||
)
|
||||
expectRequest(source).andRespond(withSuccess(objectMapper.writeValueAsString(response), MediaType.APPLICATION_JSON))
|
||||
}
|
||||
|
||||
private fun expectRequest(source: String): ResponseActions {
|
||||
val body = objectMapper.writeValueAsString(mapOf("source" to "ko", "target" to "en", "text" to source))
|
||||
return server.expect(requestTo("https://papago.apigw.ntruss.com/nmt/v1/translation"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(header("X-NCP-APIGW-API-KEY-ID", "test-client-id"))
|
||||
.andExpect(header("X-NCP-APIGW-API-KEY", "test-client-secret"))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(body, true))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user