spring mock test 에 대해 알아보자

spring mock test

이번엔 spring mock test에 대해서 알아 볼 것이다.
mock 으로 테스트를 잘 하지 않아서 익숙하지 않다.
그래서 이제부터는 mock test를 사용 하도록 노력 할라고 하는 중이다.
일단 spring boot로 할 것이다.
그래서 아래와 같이 메이븐을 추가하자.

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

일단 기본적으로 테스트에 필요한 라이브러리다.
첫번째는 json-path 여기에 자세히 나와있다.
두번째는 spring test를 위한 mock 라이브러리다.
이번에도 스칼라도 했다. 흠하

일단 테스트 클래스에 아래와 같이 어노테이션을 추가 한다.

@RunWith(classOf[SpringJUnit4ClassRunner])
@SpringApplicationConfiguration(Array(classOf[SpringBootConfig]))
@WebAppConfiguration
@FixMethodOrder(MethodSorters.JVM)

첫번째는 Spring에서 제공하는 Runner다
두번째는 Spring Boot를 사용해서 저 어노테이션을 쓴거다. 빈 들을 관리해준다.
Boot를 쓰지 않았을때는 @ContextConfiguration 어노테이션을 썼다. 물론 SpringApplicationConfiguration 안에 ContextConfiguration 포함되어 있다.
세번째는 WebApplicationContext를 생성해주는 아이다.
네번째는 필수 사항이 아니다. 테스트 순서를 정하는 거다.
테스트를 위해 아래와 같이 셋팅 해주자.

var objectMapper: ObjectMapper = _

var mockMvc: MockMvc = _

@Autowired
var wac: WebApplicationContext = _

@Before
def before: Unit = {
  objectMapper = new ObjectMapper
  mockMvc = MockMvcBuilders.webAppContextSetup(wac).build
}

objectMapper는 다들 아시다 시피 json으로 값을 넘길때 사용 할거다.
mockMvc를 선언한다.
그리고 WebApplicationContext 는 mockMvc를 생성하기 위해서 필요하다.
before 메소드에 objectMapper와 mockMvc 생성해준다.

일단 코드 부터 보자

@Test
@Test
def mockTest: Unit = {
  mockMvc.perform(get("/accounts") header ("Accept","application/json")  contentType(MediaType.APPLICATION_JSON))
    .andDo(print())
    .andExpect(status isOk)
    .andExpect(handler handlerType (classOf[AccountController]))
    .andExpect(handler methodName ("accounts"))
    .andExpect(content contentType(MediaType.APPLICATION_JSON_UTF8))
    .andExpect(jsonPath("$.content[0].name", is("wonwoo")))
    .andExpect(jsonPath("$.content[1].name", is("kevin")))
}

첫번째줄은 Http method와 url, 헤더 정보를 셋팅 할 수 있다. contentType와 accpet를 셋팅 했다. contentType처럼 메소드를 사용 할 수 있고
커스텀 header 도 설정 할 수 있다.
이 외 에도 accept, cookie, locale, sessionAttr, session, principal 등 여러가지 메소드가 존재 한다.
print() 는 request response 정보를 콘솔창에 출력해준다.
status.isOk 는 http code가 200일 경우를 체크 하는거다.
이 외 에도 isCreated, isNoContent, isBadRequest, isUnauthorized, isNotFound 등 이 있다. 자주 쓰는 것만 넣어뒀다.
웬만한 http code가 다 있는 듯하다.
handler.handlerType은 요청 컨트롤러이다.
handler methodName은 요청 메소드이다.
content.contentType은 response의 미디어 타입이다.
일단 여기 까지 성공 되었다면 다음은 데이터를 확인할 차례이다.
json-path 라이브러리를 추가한 이유이다.
문법은 저기 링크에 자세히 나와있다.
content 키를 갖고 있는 배열의 첫번째 name이 wonwoo와 같은지 비교하는거다.
만약 틀린다면 에러를 내뱉는다.

코드를 좀더 보자.

@Test
def mockTest1: Unit = {
  mockMvc.perform(get("/account/{id}", 2.asInstanceOf[Object]) contentType (MediaType.APPLICATION_JSON))
    .andDo(print())
    .andExpect(status isOk)
    .andExpect(handler handlerType (classOf[AccountController]))
    .andExpect(handler methodName ("account"))
    .andExpect(jsonPath("$.name", is("kevin")))
}

urlTemplate 처럼 만들 수도 있다.
나머지는 같지만 단일 데이터이기 때문에 $.name 이렇게 했다.

mockMvc.perform(get("/account/search") param("name", "wonwoo")

이렇게 파라미터로 보낼 수도 있다.

@Test
def mockTest4: Unit = {

  val account = new Account()
  account.setId(3L);
  account.setName("mockTest")
  account.setPassword("pwMockTest")

  mockMvc.perform(post("/account")
    contentType (MediaType.APPLICATION_JSON)
    content (objectMapper.writeValueAsString(account)))
    .andDo(print())
    .andExpect(status isCreated)
    .andExpect(jsonPath("$.name", is("mockTest")))
    .andExpect(jsonPath("$.password", is("pwMockTest")))
}

이번엔 requestbody로 보내는 데이터를 만들었다.
objectMapper 를 이용해서 엔티티빈을 json String으로 만들었다.
http code는 201로 생성 했다고 코드를 받았다.

이렇게 테스트 케이스를 만들어서 사용할 예정이다.
예전엔 그냥 크롬 확장프로그램에서 Advanced REST client를 썼는데 이젠 테스트 케이스를 만들어서 사용 해야 겠다.

spring boot 와 scala의 만남

spring boot 와 scala의 만남

스칼라를 공부할겸 겸사겸사 스칼라로 spring boot 프로젝트를 해봤다.
근데 딱히 스칼라를 제대로 쓰진 못한듯 하다. 흠 아직 왕초보라 그런지 그래도 나름 도움은 된듯 싶다.
뭔가를 만드니까 그래도 조금은 도움은 됐다.
한번 살펴보자
일단 메이븐을 추가 하자. 그래들은 잘 할 줄 몰라서.. 언젠가 공부를 해야겠다. 일단 나중에.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
</dependency>

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>0.7.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

메이븐은 일반 Spring Boot 프로젝트다. jpa와 메모리 디비를 사용할 거다.

일단 엔티티 클래스를 보자

@Entity
class Account{

  @Id
  @GeneratedValue
  @BeanProperty
  var id: java.lang.Long = _

  @BeanProperty
  @NotNull
  @Size(max = 100, min = 3)
  var name: String = _

  @BeanProperty
  var password: String = _

}

흠 딱히 눈에 띄는건 없고 @BeanProperty만 보인다. 나머지는 JPA니 다 알듯 싶다. JPA도 공부해야되는데.
책만 사놓고 조금 읽다 실습도 조금하다…멈췄지만 다시 공부해야ㅜㅜ 어쩌됬건…
BeanProperty 는 좋은 어노테이션이다.
이것은 자바스타일의 getter setter를 만들어 주는 어노테이션이다.
자동으로 getter와 setter가 생겼다.

@RestController
class AccountController @Autowired()(accountRepository: AccountRepository, accountService: AccountService) {

  @RequestMapping(value = Array("/accounts"), method = Array(RequestMethod.GET))
  @ResponseStatus(HttpStatus.OK)
  def accounts(pageable: Pageable) = accountRepository.findAll(pageable)

  @RequestMapping(value = Array("/account/{id}"), method = Array(RequestMethod.GET))
  @ResponseStatus(HttpStatus.OK)
  def account(@PathVariable id: Long) = accountService.account(id)

  @RequestMapping(value = Array("/account/search/{name}"), method = Array(RequestMethod.GET))
  @ResponseStatus(HttpStatus.OK)
  def account(@PathVariable name: String) = accountRepository.findByName(name)

  @RequestMapping(value = Array("/account"), method = Array(RequestMethod.POST))
  @ResponseStatus(HttpStatus.CREATED)
  def createAccount(@Valid @RequestBody account: Account, bindingResult: BindingResult) = {
    if (bindingResult.hasErrors) throw BadRequestException(bindingResult.getFieldError.getDefaultMessage)
    accountService.save(account)
  }

  @RequestMapping(value = Array("/account/{id}"), method = Array(RequestMethod.PATCH))
  @ResponseStatus(HttpStatus.OK)
  def updateAccount(@PathVariable id: Long, @Valid @RequestBody account: Account, bindingResult: BindingResult) = {
    if (bindingResult.hasErrors) throw BadRequestException(bindingResult.getFieldError.getDefaultMessage)
    accountService.update(id, account)
  }

  @RequestMapping(value = Array("/account/{id}"), method = Array(RequestMethod.DELETE))
  @ResponseStatus(HttpStatus.NO_CONTENT)
  def deleteAccount(@PathVariable id: Long) = {
    accountService.delete(id)
  }
}

다음은 컨트롤러다.
생성자에 Repository 와 Service를 DI 받았다.
일단 중요한건 메소드들이 짧다.
오히려 어노테이션이 더 많다. 컨트롤러에선 딱히 하는건 없어서 그런가부다. 파라미터 체크정도만 하고 서비스 혹은 레파지토리로 넘긴다.
어딜 봐도 다 스프링 코드다…ㅜㅜㅜㅜㅜㅜ 이래서 스칼라 코드가 별루 없다.흠
필자는 비지니스 로직이 딱히 없을때는 바로 레파지토리로 넘긴다. 물론 혼자 개발할때 이야기다. 그게 더 효율적이지 않나 싶다. (필자 생각)
다음은 서비스 코드다

@Service
@Transactional
class AccountService @Autowired()(accountRepository: AccountRepository) {

  @Transactional(readOnly = true)
  def account(id: Long): Account = {
    Option(accountRepository.findOne(id)) getOrElse (throw AccountNotFoundException(s"account id $id  not found"))
  }

  def save(account: Account) = {
    accountRepository.save(account)
  }

  def update(id: Long, account: Account) = {
    val oldAccount = this.account(id)
    account.setId(oldAccount.getId)
    if (!Option(account.getName).exists(_.nonEmpty))
      account.setName(oldAccount.getName)
    if (!Option(account.getPassword).exists(_.nonEmpty))
      account.setPassword(oldAccount.getPassword)
    accountRepository.save(account)
  }

  def delete(id: Long) {
    accountRepository.delete(id)
  }
}

getOrElse는 null이 아닐경우 Option() 에 들어간아이를 리턴하고 아니면 getOrElse 뒤에 있는 아이를 리턴한다.
하지만 필자는 에러를 내뿜었다.
update같은 경우엔 PATCH 메소드를 사용했다 요즘은 PATCH도 많이 사용된다고 하길래 써봤다. 뭐 어차피 비슷한지 않나 싶다
부분 업데이트냐 전체 업데이트냐 그차이 뿐이지만
그래서 저렇게 구현했다.
null 이 아니거나 비어있지 않으면 기존꺼를 넣어주고 아니면 새로운거 업데이트를 한다.

@Repository
trait AccountRepository extends JpaRepository[Account, java.lang.Long] {
  def findByName(name: String): Account
}

저기 Long 타입을 왜 자바꺼 썼냐면 JPA에서 에러를 내뱉는다.

... do not conform to trait JpaRepository's type parameter bounds [T,ID <: java.io.Serializable] 

Serializable가 안되어 있다고 하는듯 하다. 그래서 자바껄로 했다.

기본적인 구현은 다 됐다.
테스트를 해보자

@RunWith(classOf[SpringJUnit4ClassRunner])
@SpringApplicationConfiguration(Array(classOf[SpringBootConfig]))
@WebAppConfiguration
@FixMethodOrder(MethodSorters.JVM)
class AccountTest {

  var objectMapper: ObjectMapper = _

  var mockMvc: MockMvc = _

  @Autowired
  var wac: WebApplicationContext = _

  @Before
  def before = {
    objectMapper = new ObjectMapper
    mockMvc = MockMvcBuilders.webAppContextSetup(wac).build
  }

  @Test
  def accountsTest: Unit = mockMvc.perform(get("/accounts")).andDo(print()).andExpect(status.isOk)

  @Test
  def accountTest: Unit =
    mockMvc.perform(get("/account/1"))
      .andDo(print())
      .andExpect(status.isOk)
      .andExpect(jsonPath("$.name", is("wonwoo")))
      .andExpect(jsonPath("$.password", is("pw123000")))

  @Test
  def creatTest: Unit = {
    val account = new Account
    account.setName("create")
    account.setPassword("create123")

    mockMvc.perform(post("/account")
      .contentType(MediaType.APPLICATION_JSON)
      .content(objectMapper.writeValueAsString(account)))
      .andExpect(status.isCreated)
      .andDo(print())
  }

  @Test
  def updateTest: Unit = {
    val account = new Account
    account.setName("wonwoo1")
    mockMvc.perform(patch("/account/1")
      .contentType(MediaType.APPLICATION_JSON)
      .content(objectMapper.writeValueAsString(account)))
      .andDo(print())
      .andExpect(status.isOk)
      .andExpect(jsonPath("$.name", is("wonwoo1")))
      .andExpect(jsonPath("$.password", is("pw123000")))
  }

  @Test
  def deleteTest: Unit =
    mockMvc.perform(delete("/account/2").contentType(MediaType.APPLICATION_JSON))
      .andDo(print())
      .andExpect(status.isNoContent)


  @Test
  def accountNotFoundExceptionTest: Unit = mockMvc.perform(get("/account/10")).andDo(print()).andExpect(status.isBadRequest)

  @Test
  def accountBadRequestExceptionTest: Unit = {
    val account = new Account
    account.setName("wl")
    mockMvc.perform(post("/account").contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(account)))
      .andDo(print())
      .andExpect(status.isBadRequest)
  }
}

그냥 기본적인 테스트 케이스다.
Junit은 원래 자기 마음 대로 테스트 한다.
그래서 순서를 정하고 싶었다. 마침 있다. 이번에 새로 알았다.

@FixMethodOrder(MethodSorters.JVM)

요 어노테이션이다.
JVM은 있는 메소드 순서대로 테스트 하는 모양이다.
이거 말고도 한개가 더 있다. DEFAULT도 있는데 이건 그냥 기본인듯 하다.
NAME_ASCENDING 속성이다. 이거는 메소드 명 순서대로 테스트 케이스를 수행한다.

보니까 스칼라 코드가 너무 없어서 아쉽다. 나중엔 좀더 복잡한거를 해봐야겠다.
그래도 많이 도움되어서 다행이다.

이 전체 코드는 github에 올라가 있다.
https://github.com/wonwoo/spring-boot-scala.git

docker 에도 올려놨다.
배운거 다 써먹는다.ㅎㅎㅎㅎ
docker pull wonwoo/spring-boot-scala
그럼 spring boot와 스칼라의 만남은 여기서…

나중엔 블로그를 한개 만들어 봐야겠다.

스칼라 기초 문법

스칼라

trait을 알아보기전에 기초문법(?)을 알아보자
이게 사실 더 중요하니까

for

일단 for문 부터 알아보자
자바랑 약간 다르다.

val numbers = Array(19,42,85,33,100)

이런 배열이 있다고 하자
그럼 값을 꺼낼라면 이렇게 하면된다.

for(i <- numbers){
  println(i)
}

흠 처음 보는거지만 간단해졌다. 엥 나는 index를 뽑고 싶은데? 그럼 이렇게 하면된다.

for(i <- 0 to (numbers.length - 1)){
  println(numbers(i))
}

하지만 to는 마지막을 포함한다 그래서 -1을 해주었다.
이 얘기는 포함안되게 할 수도 있다는거다.

for(i <- 0 until (numbers.length)){
  println(numbers(i))
}

until 을 사용하면 된다.

흠 그럼 감소 연산은 어떻게 할까

for(i <- 10 to 0 ){
  println(i)
}

안된다.
몇가지 방법은 있다.
코드로 보자

for (i <- 10 to 0 by -1) {
  println(i)
}

이렇게 하거나

for (i <- (0 to 10).reverse){
  println(i)
}

이렇게 하면 된다. 조금 어색하긴하다.
또 한가지는 for문 자체를 변수에 담을 수 있다. 완전 1급 언어다.

val f = (s:Int) => for (i <- 0 to s) {
  println(i)
}

f(3)

while

이번에 while을 알아보겠다.
while의 syntax는 자바와 같다.

while(condition){
   statement(s);
}

이런형태의 문법을 갖고 있다.

var i = 0
while (i < 10){
  println(i)
  i+=1
}

흠 자바와 동일하다.
근데 여기서 i++ 하지 않고 i = i + 1 이런 형태로 한 이유는 없다.문법이. 스칼라에 i++이런 증감 연산하는 문법이 없다.
그래서 저렇게 해줘야 되는듯 싶다.
do while 도 동일하다.
자바와 동일해서 딱히 할 이야긴 없는거 같다.

if

이번엔 if 문이다.

if(boolean){
  //code
}else{
  //code
}

요 아이도 자바와 같다.

val value = 10;

if(value == 10){
  println(s"10 eq $value")
}else{
  println(s"10 ne $value")
}

그리고 스칼라에서 .(쩜) 닷() 을 생략 할 수 있다. 물론 ()는 파라미터가 두개 이상이면 안된다.
한번 이 코드를 보자!

class Dot {
  def twice(value:Int) =  value * 2
}

이런 클래스가 있다고 가정하자. twice는 두배를 해주는 function이다.

val dot = new Dot
print(dot.twice(10))

우리는 이렇게 호출 할 것이다.
하지만 닷과괄호가 빠진다면?

println(dot twice 10)

이렇게 해도 컴파일도 잘되고 어플리케이션도 잘된다.
왠지 익숙(?)한 문법이다. 위에서 한번 써봤다.

for(i <- 0 to 10 ){
  println(i)
}

이것도 원래는

for(i <- 0.to(10)){
  println(i)
}

이거다. 이말은 즉 숫자도 객체다. 모든것이 객체다.
0도 객체도 사용 할 수 있다.

보면 볼수록 꽤 괜찮은 언어같다.
아직 초보라 긴가민가 하는것도 많다.
열심히 해야겠다.ㅜㅜ