Spring Initializer 에서 프로젝트 2개 생성
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.6.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
id("com.google.cloud.tools.jib") version "3.1.2"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
}
group = "io.github.atlanboa"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks {
jib {
from {
image = "azul/zulu-openjdk-alpine:11.0.13"
}
to {
image = "docker.akmj.io/${rootProject.name}"
tags = mutableSetOf("${project.version}", "latest")
credHelper = "osxkeychain"
}
container {
mainClass = "io.github.atlanboa.simpleapiserver.SimpleApiServerApplication"
volumes = mutableListOf("/tmp")
}
}
}
api test 를 위해서 controller 에 method 하나 생성
@RestController
@RequestMapping("/simple")
class Controller {
@Value("\${akmj.name}")
private lateinit var author: String
@GetMapping
fun getAnyRequest(@RequestParam objectName: String): ResponseEntity<String> {
return ResponseEntity.ok("this is any response with $objectName by $author")
}
}
app 실행 ain method 수정
@SpringBootApplication
class SimpleApiServerApplication {
companion object {
@JvmStatic
fun main(args: Array<String>) {
runApplication<SimpleApiServerApplication>(*args)
}
}
}
port 값 할당
testContainer 생성시에 환경 변수 주입을 위해서 akmj.name 하나 추가
server:
port: 8889
akmj:
name: ${AUTHOR_NAME:"akmj"}
로컬 도커 이미지 빌드하려면, 로컬에서 도커가 실행되어야 함.
build.gradle.kts 에 jib 추가하면 jib gradle task 가 생김.
여기서 jibDockerBuild 실행
terminal 띄워서 docker images 쳐서 해당 이미지 있는지 확인.
다음처럼 생성됬으면 simple api 은 끝
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.6.7"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
}
group = "io.github.atlanboa"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
// annotation
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
// test container
implementation(platform("org.testcontainers:testcontainers-bom:1.16.2"))
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:rabbitmq")
testImplementation("org.testcontainers:localstack")
// rest assured
testImplementation("io.rest-assured:kotlin-extensions:4.4.0")
// aws
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
// test container
implementation(platform("org.testcontainers:testcontainers-bom:1.16.2"))
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:rabbitmq")
testImplementation("org.testcontainers:localstack")
testImplementation("io.rest-assured:kotlin-extensions:4.4.0")
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE")
server:
port: 8888
# s3Client bean 오버라이딩해야함.
spring:
main:
allow-bean-definition-overriding: true
# 테스트할 외부 컨테이너 서버 url 필요
akmj:
simple-api-server:
url: ${SIMPLE_API_SERVER_URL:http://localhost:8889/simple}
# aws 셋업
cloud:
aws:
s3:
bucket: fake-bucket
region:
static: ap-northeast-2
credentials:
accessKey: ACCESSKEY
secretKey: SECRETKEY
instance-profile: true
stack:
auto: false
# aws s3 access 관련해서 문제없어도 warning 뜨는데, 보기 안좋음. 안뜨게 설정
logging:
level:
com:
amazonaws:
util:
EC2MetadataUtils: error
restTemplate 이랑 s3Client Bean 생성해줌.
@Configuration
class ProjectConfig {
@Value("\${cloud.aws.credentials.accessKey}")
private val accessKey: String? = null
@Value("\${cloud.aws.credentials.secretKey}")
private val secretKey: String? = null
@Value("\${cloud.aws.region.static}")
private val region: String? = null
@Bean
fun restTemplate(): RestTemplate {
return RestTemplateBuilder().build()
}
@Bean
fun s3Client(): AmazonS3 {
val credentials = BasicAWSCredentials(this.accessKey, this.secretKey)
return AmazonS3ClientBuilder.standard()
.withCredentials(AWSStaticCredentialsProvider(credentials))
.withRegion(this.region)
.enablePayloadSigning()
.build()
}
}
@RestController
@RequestMapping("/first")
class Controller @Autowired constructor(
private val restTemplate: RestTemplate,
private val s3Client: AmazonS3
) {
@Value("\${cloud.aws.s3.bucket}")
private lateinit var bucket: String
@Value("\${akmj.simple-api-server.url}")
private lateinit var simpleApiUrl: String
@GetMapping
fun getS3ObjectName(): ResponseEntity<String> {
if (s3Client.doesObjectExist(this.bucket, "object")) {
val urlBuilder = UriComponentsBuilder
.fromHttpUrl("$simpleApiUrl?objectName=object")
val response = restTemplate.exchange(
urlBuilder.toUriString(),
HttpMethod.GET,
HttpEntity(null, null),
String::class.java
)
return ResponseEntity.ok(response.body)
}
return ResponseEntity.ok("object name query failed")
}
}
테스트의 목적은 AWS S3 Storage 를 직접 사용하는게 아닌, LocalStackContainer 로 띄우고, Simple Api Server 연동 테스트 또한 TestContainer 로 띄워 해당 로직 테스트
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.AmazonS3ClientBuilder
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.DependsOn
import org.testcontainers.containers.localstack.LocalStackContainer
import org.testcontainers.utility.DockerImageName
@TestConfiguration
class IntegrationTestConfig {
@Bean
@DependsOn("s3Container")
fun s3Client(localStackContainer: LocalStackContainer): AmazonS3 {
return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(localStackContainer.getEndpointConfiguration(LocalStackContainer.Service.S3))
.withCredentials(localStackContainer.defaultCredentialsProvider)
.build()
}
@Bean(initMethod = "start", destroyMethod = "stop")
fun s3Container(): LocalStackContainer {
val localstackImage = DockerImageName.parse("localstack/localstack:0.11.3")
return LocalStackContainer(localstackImage)
.withServices(LocalStackContainer.Service.S3)
}
}
해당 클래스에서 정의해야 될 내용은 다음과 같음.
1. 임의로 생성한 Simple Api Server Container
2. First Server 의 Property akmj.simple-api-server.url 값 주입
3, rest assured set up
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import io.restassured.RestAssured
import io.restassured.builder.RequestSpecBuilder
import io.restassured.config.LogConfig
import io.restassured.config.RestAssuredConfig
import io.restassured.filter.log.LogDetail
import io.restassured.http.ContentType
import io.restassured.module.kotlin.extensions.Extract
import io.restassured.module.kotlin.extensions.Given
import io.restassured.module.kotlin.extensions.Then
import io.restassured.module.kotlin.extensions.When
import io.restassured.specification.RequestSpecification
import org.apache.http.HttpStatus
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.testcontainers.containers.GenericContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.io.ByteArrayInputStream
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Testcontainers
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [IntegrationTestConfig::class])
internal class IntegrationTest {
companion object {
@Container
val container =
GenericContainer("docker.akmj.io/simple-api-server:0.0.1-SNAPSHOT")
.apply {
withExposedPorts(8889)
withEnv(mutableMapOf("AUTHOR_NAME" to "akmj"))
start()
}
@DynamicPropertySource
@JvmStatic
fun properties(registry: DynamicPropertyRegistry) {
registry.add("akmj.simple-api-server.url") {
"http://${container.containerIpAddress}:${container.getMappedPort(8889)}/simple"
}
}
}
@Autowired
private lateinit var s3Client: AmazonS3
private val objectMapper = ObjectMapper()
@LocalServerPort
private var port: Int = 0
private lateinit var requestSpec: RequestSpecification
@BeforeAll
fun setUp() {
val logConfig = LogConfig.logConfig()
.enableLoggingOfRequestAndResponseIfValidationFails(LogDetail.ALL)
val config = RestAssuredConfig.config().logConfig(logConfig)
this.requestSpec = RequestSpecBuilder()
.setBaseUri("http://localhost:${this.port}")
.setContentType(ContentType.JSON)
.setConfig(config)
.build()
val inputStream = ByteArrayInputStream("fake object".toByteArray())
this.s3Client.createBucket("fake-bucket")
this.s3Client.putObject("fake-bucket", "object", inputStream, ObjectMetadata())
}
@AfterAll
fun tearDown() {
RestAssured.reset()
}
@Test
fun `api test`() {
val msg = Given {
spec(requestSpec)
} When {
get("/first")
} Then {
statusCode(HttpStatus.SC_OK)
} Extract {
response().asString()
}
assertEquals("this is any response with object by akmj", msg)
}
}
companion object {
@Container
val container =
GenericContainer("docker.akmj.io/simple-api-server:0.0.1-SNAPSHOT")
.apply {
withExposedPorts(8889)
withEnv(mutableMapOf("AUTHOR_NAME" to "akmj"))
start()
}
@DynamicPropertySource
@JvmStatic
fun properties(registry: DynamicPropertyRegistry) {
registry.add("akmj.simple-api-server.url") {
"http://${container.containerIpAddress}:${container.getMappedPort(8889)}/simple"
}
}
}
simple api server 도커 컨테이너를 띄우기 위한 부분.
@Autowired
private lateinit var s3Client: AmazonS3
private val objectMapper = ObjectMapper()
@LocalServerPort
private var port: Int = 0
private lateinit var requestSpec: RequestSpecification
@BeforeAll
fun setUp() {
val logConfig = LogConfig.logConfig()
.enableLoggingOfRequestAndResponseIfValidationFails(LogDetail.ALL)
val config = RestAssuredConfig.config().logConfig(logConfig)
this.requestSpec = RequestSpecBuilder()
.setBaseUri("http://localhost:${this.port}")
.setContentType(ContentType.JSON)
.setConfig(config)
.build()
val inputStream = ByteArrayInputStream("fake object".toByteArray())
this.s3Client.createBucket("fake-bucket")
this.s3Client.putObject("fake-bucket", "object", inputStream, ObjectMetadata())
}
@AfterAll
fun tearDown() {
RestAssured.reset()
}
테스트 전에 bucket set up 해주는 작업
requestSpecification set up
@Test
fun `api test`() {
val msg = Given {
spec(requestSpec)
} When {
get("/first")
} Then {
statusCode(HttpStatus.SC_OK)
} Extract {
response().asString()
}
assertEquals("this is any response with object by akmj", msg)
}
이 로직을 테스트하게 되면, S3 Storage, Simple Api Server 가 Docker Container 로 실행된 이후 해당 API 요청 테스트가 진행이 됨.