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와 스칼라의 만남은 여기서…

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

Spring boot와 Docker Postgresql

Spring boot와 Docker Postgresql

이번엔 docker와 posrgresql에 대해 알아보겠다.
1. centos 6에 docker를 설치해보자
2. Spring boot와 Docker
3. Spring boot와 Docker Mysql

mysql과 소스는 동일하다.

@SpringBootApplication
public class SpringBootDockerPostgresqlApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootDockerPostgresqlApplication.class, args);
    }

    @Autowired
    private AccountRepository repository;

    @Bean
    CommandLineRunner runner() {
        return args -> Arrays.asList(
                new Account(1L,"wonwoo","wonwoo@test.com"),
                new Account(2L,"kevin","kevin@test.com"),
                new Account(3L,"mink","mink@test.com")
        ).forEach(repository::save);
    }
}

Account class

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {

    @Id
    @GeneratedValue
    private Long id;

    private String username;

    private String email;

}

AccountController class

@Slf4j
@RestController
public class AccountController {

    @Autowired
    private AccountRepository repository;

    @RequestMapping(value = "/accounts", method = RequestMethod.GET)
    public ResponseEntity<?> getAccount() {
        List<Account> accounts = repository.findAll();
        log.debug("accounts : {} ", accounts);
        return new ResponseEntity<>(accounts, HttpStatus.OK);
    }
}

AccountRepository class

@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
}

따로 설명은 하지 않겠다.
mysql과 다른점은 yml 파일과 maven이다.

application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: create
    database-platform: org.hibernate.dialect.PostgreSQLDialect
  datasource:
    url: jdbc:postgresql://${POSTGRES_PORT_5432_TCP_ADDR}:${POSTGRES_PORT_5432_TCP_PORT}/docker
    username: dbuser
    password: dbpassword
    driver-class-name: org.postgresql.Driver

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.4-1200-jdbc4</version>
</dependency>

일단 postgresq pull 하자

docker run --name spring-boot-postgres -e POSTGRES_PASSWORD=dbpassword -e POSTGRES_DB=docker -d postgres

latest: Pulling from postgres
00b8e8227087: Pull complete 
1ad190f65f04: Pull complete 
60f2f5fb5bf9: Pull complete 
2d0c73368a74: Pull complete 
a786535117f1: Pull complete 
dd182ad340e4: Pull complete 
c05a4dbabb61: Pull complete 
d147dcf8581e: Pull complete 
358e57fb3884: Pull complete 
9c4b8b3535ba: Pull complete 
adf658a6b808: Extracting [==================================================>] 41.23 MB/41.23 MB
0fc677feeba3: Download complete 
0dceace87c23: Download complete 
23b5786d3dce: Download complete 
9b00a7295803: Download complete 
f88f356d246d: Download complete 
d6fb5e6e70f6: Download complete 
07e6c1333fa3: Download complete 
6f960967c8bf: Download complete 
d8bd0657b25f: Already exists 
a582cd499e0f: Already exists 

위와 같이 이미지를 생성한다.

docker images

이미지가 잘 있나 보자

postgres                    latest              6f960967c8bf        5 days ago          264.5 MB

잘있다.

다음으로 메이븐 빌드를 하자!

mvn clean package docker:build

빌드가 잘 되었다면

docker를 실행시키자

docker run --name spring-boot-docker-postgresql --link spring-boot-postgres:postgres -p 9000:8080 -d wonwoo/spring-boot-docker-postgresql

로그를 확인하기 위해 아래와 같이 입력하자

docker logs $CONTAINER_ID 

잘되었다면 브라우저를 열어 확인해보자
http://localhost:8080/accounts

그리고 postgres 클라이언트 연결도 가능하다.

docker run -it --link spring-boot-postgres:postgres --rm postgres sh -c 'exec psql -h "$POSTGRES_PORT_5432_TCP_ADDR" -p "$POSTGRES_PORT_5432_TCP_PORT" -U postgres'

위와 같이하면 접속도 가능하다.

postgres=# \l

                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges   
-----------+----------+----------+------------+------------+-----------------------
 docker    | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 | 
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
(4 rows)

디비 목록이다.

postgres=# \connect docker

선택을 해보자

docker=# \d

테이블을 보자. account 테이블이 잘 있다.

                 List of relations
 Schema |        Name        |   Type   |  Owner   
--------+--------------------+----------+----------
 public | account            | table    | postgres
 public | hibernate_sequence | sequence | postgres
(2 rows)

한번씩 해보자!