BDD(Behavior-Driven Development)

&

Spock Framework

 

정현일

목차

  1. BDD 란?
  2. Spock Framework
  3. 요구사항 분석부터 명세 작성하기 까지
  4. JUnit과 Mockito를 통한 BDD
  5. Spock Framework를 통한 BDD

BDD 란?

TDD

코드가 없는데 무엇을 테스트?

BDD

  • TDD에서 파생된 개발 방법론
  • 코드의 구현과 테스트보다 행위(동작, 명세)에 집중
  • 기능의 테스트 케이스를 작성하는 것이 아닌 명세를 작성
  • 요구사항 분석 > 기능 설계 및 명세 작성 > 코드 구현

BDD

Title: 스토리에대한 제목을 간략하고 명확하게 작성

User Story

  • Who
  • Why
  • What

Scenario

  • Given
  • When
  • Then

Spock Framework

Spock Framework

  • Java와 Groovy 어플리케이션을 위한 명세 프레임워크
  • Groovy(DSL)
    • 간결함
    • 직관적
  • JUnit, Hamcrest, Mockito를 전부 다 학습하는 것보다 손쉬움
  • Mock, Stub, Spy 사용이 편리
  • 실패에 대한 로그를 직관적으로 보여줌

Spock Framework

  • setup: 메소드 실행 전에 실행(given)
  • when: 행위에 대한 명세를 작성
  • then: 행위에 대한 예측을 작성
  • expect: 행위에 대한 명세와 예측을 작성(when + then)
  • cleanup: 메소드 실행 후에 실행
  • where: 여러 값에 대해 반복행위를 할 때 작성

Spock Framework

def "zeroIfNotPresent #value가 null이면 0을 반환한다."() {
  given: "value는 null이다."
  def value = null

  when: "zeroIfNotPresent를 실행한다."
  def result = [
      OptionalUtil.zeroIfNotPresent((Integer)value),
      OptionalUtil.zeroIfNotPresent((Long)value),
      OptionalUtil.zeroIfNotPresent((Float)value),
      OptionalUtil.zeroIfNotPresent((Double)value)
  ]

  then: "#result는 0이다."
  result == [0, 0L, 0f, 0.0]
}

Spock Framework

def "falseIfNotPresent #value가 null이면 false를 반환하고 null이 아니면 #value를 반환한다."() {
  expect:
  result == OptionalUtil.falseIfNotPresent(value)

  where:
  value || result
  true  || true
  false || false
  null  || false
}
def "throwIllegalArgumentExceptionIfNotPresent #value가 null이면 IllegalArgumentException을 throw한다."() {
  given: "value는 null이다."
  def value = null

  when: "throwIllegalArgumentExceptionIfNotPresent를 실행한다."
  OptionalUtil.throwIllegalArgumentExceptionIfNotPresent(value)

  then: "IllegalArgumentException이 발생한다."
  thrown(IllegalArgumentException)
}

Spock Framework

요구사항 분석부터

명세 작성하기 까지

Todo Management System

고객의 요구사항

할일 목록 관리할 수 있는 프로그램 만들어주세요~

요구사항 분석

  • 필요한 기능에 대해 분석
  • 고객과 지속적인 커뮤니케이션
  • 스펙에 대한 정의

할일을 관리할 수 있는 시스템을 개발한다.
Todo Item을 등록/수정/삭제 할 수 있다.
Todo/Doing/Done 할 수 있고 Archive할 수 있어야 한다.
Todo Item을 등록할 때 Todo 상태로 시작한다.

상태변경을 할 수 있고 상태 변경은
Todo > Doing, Doing > Done, Done > Doing, Doing > Todo로만 할 수 있다.

Archive는 Todo/Doing/Done 모든 상태에서 가능하다.
목록보기/상세보기 기능을 포함한다.
페이징 기능은 스펙에서 제외하고 현재 스펙에서는 전체 목록을 한번에 조회한다.

기능 설계

할일을 관리할 수 있는 시스템을 개발한다.
Todo Item을 등록/수정/삭제 할 수 있다.
Todo/Doing/Done 할 수 있고 Archive할 수 있어야 한다.
Todo Item을 등록할 때 Todo 상태로 시작한다.

상태변경을 할 수 있고 상태 변경은
Todo > Doing, Doing > Done, Done > Doing, Doing > Todo로만 할 수 있다.

Archive는 Todo/Doing/Done 모든 상태에서 가능하다.
목록보기/상세보기 기능을 포함한다.
페이징 기능은 스펙에서 제외하고 현재 스펙에서는 전체 목록을 한번에 조회한다.

명세 작성(Title & User Story)

Title: Todo Item의 상태를 변경한다.

 

User Story

Who: Todo Management System을 사용하는 사용자가

Why: Todo Item의 상태관리를 위해서

What: 각각의 Todo Item의 상태를 변경할 수 있다.

           상태 변경은 Todo > Doing, Doing > Done,

           Done > Doing, Doing > Todo로만 할 수 있다.

명세작성(Scenario)

Scenario 1: Todo상태를 Doing상태로 변경하면 상태가 변경된다.

 

Scenario 2: Doing상태를 Done상태로 변경하면 상태가 변경된다.

 

Scenario 3: Done상태를 Doing상태로 변경하면 상태가 변경된다.

 

Scenario 4: Doing상태를 Todo상태로 변경하면 상태가 변경된다.

 

Scenario 5: Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.

 

Scenario 6: Done상태를 Todo상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.

JUnit과 Mockito를
통한 BDD

JUnit과 Mockito를 통한 BDD

import kr.pe.nuti.home.api.core.application.Application;
import kr.pe.nuti.home.api.core.application.JpaConfiguration;
import kr.pe.nuti.home.api.core.application.WebConfiguration;
import kr.pe.nuti.home.api.domain.todo.TodoItem;
import kr.pe.nuti.home.api.enumeration.todo.TodoState;
import kr.pe.nuti.home.api.exception.todo.IllegalStateChangeException;
import kr.pe.nuti.home.api.repository.todo.TodoItemRepository;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Optional;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

JUnit과 Mockito를 통한 BDD

/**
 * Title: Todo Item의 상태를 변경한다.
 * User Story:
 * Todo Management System을 사용하는 사용자가
 * Todo Item의 상태관리를 위해서
 * 각각의 Todo Item의 상태를 변경할 수 있다.
 * 상태 변경은 Todo > Doing, Doing > Done,
 * Done > Doing, Doing > Todo로만 할 수 있다.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JpaConfiguration.class, WebConfiguration.class, Application.class})
public class TodoServiceStateChangeTest {

  @Mock
  private TodoItemRepository todoItemRepository;

  @Autowired
  @Spy
  @InjectMocks
  private TodoService service;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
  }
}

JUnit과 Mockito를 통한 BDD

/**
 * Todo상태를 Doing상태로 변경하면 상태가 변경된다.
 */
@Test
public void testStateChangeFromTodoToDoing() throws Exception {
  // given Todo 상태의 Todo Item
  TodoItem savedItem = new TodoItem();
  savedItem.setIdx(1L);
  savedItem.setState(TodoState.TODO);
  TodoItem changedItem = new TodoItem();
  changedItem.setIdx(1L);
  changedItem.setState(TodoState.DOING);
  when(todoItemRepository.findById(any(Long.class))).thenReturn(Optional.of(savedItem));
  when(todoItemRepository.save(any(TodoItem.class))).thenReturn(changedItem);

  TodoItem item = new TodoItem();
  item.setIdx(1L);

  // when Todo Item의 상태를 Doing으로 변경한다.
  TodoItem result = service.changeState(item, TodoState.DOING);

  // then Todo Item의 상태가 Doing으로 변경된다.
  Assert.assertThat(result.getState(), is(TodoState.DOING));
  verifyNoMoreInteractions(service);
}

JUnit과 Mockito를 통한 BDD

/**
 * Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.
 */
@Test(expected = IllegalStateChangeException.class)
public void testStateChangeFromTodoToDoneThrownException() throws Exception {
  try {
    // given Todo 상태의 Todo Item
    TodoItem savedItem = new TodoItem();
    savedItem.setIdx(1L);
    savedItem.setState(TodoState.TODO);
    TodoItem changedItem = new TodoItem();
    changedItem.setIdx(1L);
    changedItem.setState(TodoState.DOING);
    when(todoItemRepository.findById(any(Long.class))).thenReturn(Optional.of(savedItem));
    when(todoItemRepository.save(any(TodoItem.class))).thenReturn(changedItem);

    TodoItem item = new TodoItem();
    item.setIdx(1L);

    // when Todo Item의 상태를 Done으로 변경한다.
    service.changeState(item, TodoState.DONE);

    // then Todo Item의 상태가 변경되지 않고 예외사항이 발생한다.
  } catch (Exception e) {
    verifyNoMoreInteractions(service);
    throw e;
  }
}

Spock Framework를
통한 BDD

Spock Framework를 통한 BDD

import kr.pe.nuti.home.api.domain.todo.TodoItem
import kr.pe.nuti.home.api.enumeration.todo.TodoState
import kr.pe.nuti.home.api.exception.todo.IllegalStateChangeException
import kr.pe.nuti.home.api.repository.todo.TodoItemRepository
import spock.lang.Issue
import spock.lang.Narrative
import spock.lang.See
import spock.lang.Specification
import spock.lang.Title

Spock Framework를 통한 BDD

@Title("Todo Item의 상태를 변경한다.")
@Narrative("""
Todo Management System을 사용하는 사용자가
Todo Item의 상태관리를 위해서
각각의 Todo Item의 상태를 변경할 수 있다.
상태 변경은 Todo > Doing, Doing > Done,
Done > Doing, Doing > Todo로만 할 수 있다.
""")
class TodoServiceStateChangeSpec extends Specification {

  TodoService service
  def todoItemRepository

  def setup() {
    todoItemRepository = Mock(TodoItemRepository)
    service = Spy(TodoService)
    service.todoItemRepository = todoItemRepository
  }
}

Spock Framework를 통한 BDD

@See(["https://github.com/hyeonil/smart-home-api/issues/6"])
@Issue("#6")
def "Todo상태를 Doing상태로 변경하면 상태가 변경된다."() {
  given: "Todo 상태의 Todo Item"
  TodoItem savedItem = new TodoItem([idx: 1L, state: TodoState.TODO])
  TodoItem changedItem = new TodoItem([idx: 1L, state: TodoState.DOING])
  todoItemRepository.findById(_) >> Optional.of(savedItem)
  todoItemRepository.save(_) >>  changedItem

  TodoItem item = new TodoItem([idx: 1L])

  when: "Todo Item의 상태를 Doing으로 변경한다."
  def result = service.changeState(item, TodoState.DOING)

  then: "Todo Item의 상태가 Doing으로 변경된다."
  1 * service.getItem(_)
  result.state == TodoState.DOING
}

Spock Framework를 통한 BDD

@See(["https://github.com/hyeonil/smart-home-api/issues/6"])
@Issue("#6")
def "Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다."() {
  given: "Todo 상태의 Todo Item"
  TodoItem savedItem = new TodoItem([idx: 1L, state: TodoState.TODO])
  todoItemRepository.findById(_) >> Optional.of(savedItem)

  TodoItem item = new TodoItem([idx: 1L])

  when: "Todo Item의 상태를 Done으로 변경한다."
  service.changeState(item, TodoState.DONE)

  then: "Todo Item의 상태가 변경되지 않고 예외사항이 발생한다."
  1 * service.getItem(_)
  thrown(IllegalStateChangeException)
}

References

  • https://en.wikipedia.org/wiki/Behavior-driven_development
  • https://en.wikipedia.org/wiki/User_story
  • http://spockframework.org/spock/docs/1.1/index.html
  • https://github.com/pkainulainen/spock-examples
  • https://d2.naver.com/helloworld/568425
  • https://yangbongsoo.gitbooks.io/study/content/junit+mockito_vs_groovy+spock.html

Examples

  • https://github.com/hyeonil/smart-home-api

Q & A

BDD & Spock Framework

By Hyeonil Jeong

BDD & Spock Framework

  • 860