BDD(Behavior-Driven Development)
&
Spock Framework
정현일
목차
- BDD 란?
- Spock Framework
- 요구사항 분석부터 명세 작성하기 까지
- JUnit과 Mockito를 통한 BDD
- 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