Skip to content

Instantly share code, notes, and snippets.

@mike-neck
Created October 20, 2020 01:28
Show Gist options
  • Select an option

  • Save mike-neck/86fc8d4dfe3ee2905f74fd2066575c1a to your computer and use it in GitHub Desktop.

Select an option

Save mike-neck/86fc8d4dfe3ee2905f74fd2066575c1a to your computer and use it in GitHub Desktop.
Spring WebMvc のテスト
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.3.4.RELEASE"
id("io.spring.dependency-management") version "1.0.10.RELEASE"
kotlin("jvm") version "1.3.72"
kotlin("plugin.spring") version "1.3.72"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
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") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation("org.springframework.security:spring-security-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
package com.example
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import kotlin.IllegalStateException
import java.time.Clock
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.TextStyle
import java.util.*
import kotlin.reflect.KClass
@SpringBootApplication
class ControllerTestWithSecurityApplication {
@Bean
fun clock(): Clock = Clock.systemUTC()
@Bean
fun defaultTimeZone(): DefaultTimeZone = DefaultTimeZone(ZoneId.of("UTC").getDisplayName(TextStyle.SHORT, Locale.US))
}
@Configuration
class SecurityConfig: WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity?): Unit =
if (http == null) throw IllegalStateException("http security should be not null")
else http.authorizeRequests { it.anyRequest().authenticated() }
.oauth2Login()
.let { Unit }
}
fun main(args: Array<String>) {
runApplication<ControllerTestWithSecurityApplication>(*args)
}
class DefaultTimeZone(val value: String) {
fun offset(instant: Instant): ZoneOffset = ZoneId.of(value).rules.getOffset(instant)
}
data class CurrentTime(val now: Long, val nowStr: String, val iso: String, val timeZone: String) {
constructor(offsetDateTime: OffsetDateTime, timeZone: String):
this(
offsetDateTime.toInstant().toEpochMilli(),
offsetDateTime.toInstant().toEpochMilli().toString(),
offsetDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
timeZone)
constructor(instant: Instant, defaultTimeZone: DefaultTimeZone):
this(instant.atOffset(defaultTimeZone.offset(instant)), defaultTimeZone.value)
constructor(clock: Clock, defaultTimeZone: DefaultTimeZone): this(Instant.now(clock), defaultTimeZone)
}
inline fun <reified T: Any> logger(klass: KClass<T> = T::class): Logger = LoggerFactory.getLogger(klass.java)
interface TimeService {
fun currentTime(timeZone: String?): CurrentTime
}
@Component
@Order(10)
class TimeServiceImpl(
private val clock: Clock,
private val defaultTimeZone: DefaultTimeZone): TimeService {
override fun currentTime(timeZone: String?): CurrentTime =
if (timeZone.isNullOrEmpty())
CurrentTime(clock, defaultTimeZone)
else CurrentTime(clock, DefaultTimeZone(timeZone))
}
@RestController
@RequestMapping("time")
class TimeController(
private val timeService: TimeService) {
@GetMapping(produces = ["application/vnd.example.v3+json"])
fun get(@RequestParam timeZone: String?, @AuthenticationPrincipal user: OAuth2User): CurrentTime =
logger.info("request = timeZone: {}, user: {}, {}", timeZone, user.authorities, user.attributes).let {
timeService.currentTime(timeZone)
}
companion object {
@JvmStatic
private val logger = logger<TimeController>()
}
}
package com.example
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.mockito.stubbing.OngoingStubbing
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.mock.web.MockHttpSession
import org.springframework.security.core.context.SecurityContextImpl
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken
import org.springframework.security.oauth2.core.user.DefaultOAuth2User
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority
import org.springframework.security.web.context.HttpSessionSecurityContextRepository
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.OffsetDateTime
import java.time.ZoneOffset
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = [ControllerTestWithSecurityApplication::class]
)
@AutoConfigureMockMvc
class MockMvcTests {
@Autowired
lateinit var mvc: MockMvc
@Test
fun get() {
mvc.perform(MockMvcRequestBuilders.get("/time").session(Session))
.andExpect(status().isOk)
.andExpect(jsonPath("$.timeZone").value("UTC"))
.andExpect(jsonPath("$.now").isNumber)
.andExpect(jsonPath("$.nowStr").isString)
.andExpect(jsonPath("$.iso").isString)
}
}
object Session: MockHttpSession() {
init {
setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextImpl(principal()))
}
private inline fun <A: Any, B: Any> A.withMapping(f: (A) -> B): Pair<A, B> = this to f(this)
private fun principal(): OAuth2AuthenticationToken =
mapOf("sub" to "00aa11bb22cc", "email" to "user@example.com")
.withMapping { listOf(OAuth2UserAuthority("ROLE_USER", it)) }
.withMapping { DefaultOAuth2User(it.second, it.first, "sub") }
.let { OAuth2AuthenticationToken(it.second, it.first.second, "0011-client-id") }
}
@WebMvcTest(TimeController::class)
class WebMvcMockServiceTests {
@Autowired
lateinit var mvc: MockMvc
@MockBean
lateinit var service: TimeService
@Test
fun get() {
calling(service.currentTime(null)).thenReturn(CurrentTime(fixedTime, "UTC"))
mvc.perform(MockMvcRequestBuilders.get("/time").session(Session))
.andExpect(status().isOk)
.andExpect(jsonPath("$.timeZone").value("UTC"))
.andExpect(jsonPath("$.now").value(fixedTime.toInstant().toEpochMilli()))
.andExpect(jsonPath("$.nowStr").isString)
.andExpect(jsonPath("$.iso").isString)
}
companion object {
val fixedTime: OffsetDateTime = OffsetDateTime.of(2020, 1, 2, 15, 4, 5, 600_000_000, ZoneOffset.ofHours(9))
fun <T> calling(any: T): OngoingStubbing<T> = `when`(any)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment