개념 위주인 Basic 내용에 이어 '애완동물(spring-petclinic)' 어플리케이션 코드 대상으로 실제 테스트 코드를 작성하고 커버리지를 측정하는 교육입니다. 명세로부터 테스트를 도출하는 블랙박스 테스트 접근 이후, 코드 커버리지 정보로부터 추가 테스트를 작성하는 화이트박스 기법을 차례로 적용하고 있습니다
1. 코드 레벨 테스트와 커버리지
Basic / Advanced / Expert
샘플 어플리케이션(스프링 기반 petclinic) 기반으로
Junit 테스트 사례 살펴보기
2. 목차
1. 샘플 어플리케이션(애완동물 병원) 개요
2. 테스트 코드 작성 개요
. 각 레이어(Controller/Service/Repository)별 특성
. 코드 테스트에서의 단위 / 통합 테스트란
. 테스트와 테스트 커버리지와의 관계
3. 등록 기능에 대한 테스트 접근 1 – 블랙박스
4. 등록 기능에 대한 테스트 접근 2 – 화이트 박스
5. 정리
4. 애완동물 병원- spring-framework-petclinic
http://projects.spring.io/spring-petclinic/
(유스 케이스)
수의사 목록 및 그들의 전문 분야보기
애완 동물 주인에 관한 정보보기
애완 동물 주인에 관한 정보 업데이트
시스템에 새 애완 동물 주인 추가
애완 동물에 관한 정보보기
애완 동물에 관한 정보 업데이트
시스템에 새 애완 동물 추가
애완 동물의 방문 기록과 관련된 정보보기
애완 동물의 방문 기록과 관련 정보 추가
(비즈니스 규칙)
한명의 소유자는 대소 문자를 구분없이 같은 이름으로 여러 애완
동물을 가질 수 없습니다
1. 샘플 어플리케이션(애완동물병원)개요
5. 샘플 어플리케이션 구성
출처: https://www.slideshare.net/AntoineRey/spring-framework-petclinic-sample-application
PetController
ClinicService
ClinicServiceImpl
PetRepository VetRepository
OwnerRepository VisitRepository
개발 코드
애완동물 등록 관련 클래스
1. 샘플 어플리케이션(애완동물병원)개요
7. ※ 각 레이어(Controller/Service/Repository)별 특성
Presentation Layer Business Layer Persistence Layer
설 명
사용자의 요청을 받아들여 적
절한 서버단의 비즈니스 로직
과 연결해주고 요청처리 결과
를 받아들여 사용자의 목적에
맞게 가공하여 보여주는 일을
하는 계층
어플리케이션의 실제 비즈니
스 로직이 구현되어 있는 부분
으로, 모든 비즈니스 엔터티들
과 이를 가지고 구체적인 업무
를 수행하는 비즈니스 프로세
스들이 구현되어 있다
어플리케이션의 모든 DB 관련
작업을 담당한다. DB 작업을 비
즈니스 프로세스로부터 분리시
켜 Data Access Object(DAO)에
서 처리함으로써 차후에 데이
터베이스 서버 홖경이 변경되
더라도 유연하게 대처할 수 있
게 된다
테스트 목
표
사용자의 요청을 적절한 서버
단의 비즈니스 로직과 연결해
주고 그 결과를 적절한 사용자
화면으로 반홖하는지 확인한다
어플리케이션의 실제 비즈니
스 로직이 맞게 구현되었는지
확인한다
DB 관련 작업(조회,등록,수정,
삭제)들이 정상적으로 수행되
는지 확인한다
Junit 테스
트
우선 순위
낮음
(웹 리소스를 많이 사용하는 계
층이기 때문에 Junit 으로 별도
로 테스트하기가 어렵움, 결함
이 발생할 소지가 적으며, 결함
이 쉽게 발견되고, 수정비용이
적기 때문에 일반적으로는
junit으로 테스트를 수행하지
않음)
높음
(실제 비즈니스 로직이 맞게 구
현되었는지 반드시 검증이 되
어야 하며, 개발 완료된 DAO와
연결한 경우 DAO도 같이 테스
트가 가능하다)
중간
(DB 조회,등록,수정,삭제 등이
정상적으로 수행되는지 확인이
필요하며, 비즈니스 클래스의
로직이 복잡하거나 코드가 많
은 경우 결함의 발생 위치를 빨
리 찾기 위해 DAO에 대한 junit
테스트가 꼭 필요해 짂다)
2. 테스트코드 작성 개요
어떤 레이어, 어떤 대상에 대해 테스트 코드를 작성해야 할까?
8. 코드 테스트에서의 단위 / 통합 테스트란
[ 단위 vs 통합 테스트 ]
단위 vs 통합으로 볼 수 있는 구분
1) 개별 메소드 vs 전체 클래스
개별 메소드별 테스트 vs 조회→등록 →수정→삭제등의 호출 흐름 테스트
2) 개별 클래스 vs 관련 클래스 묶음
클래스 하나에 대한 테스트vs 관련 클래스 여러 개에 대한 테스트
3) 개별 기능(API) vs 업무흐름 테스트
REST API 같은 기능 단위 테스트 vs 업무 흐름을 연동한 테스트
2. 테스트코드 작성 개요
이롞 적용?
단위 테스트 vs 통합 테스트? 코드 테스트는 어떤 테스트이지?
9. 테스트와 커버리지와의 관계
2. 테스트코드 작성 개요
- 테스트 커버리지는 단지 실행이 되었는지를 표시할 뿐 테스트 결과가 유효한지를
판단해 주지는 못한다
- 다양한 커버리지 아이템 중 한 가지만 선택하기 때문에 100% 테스트 커버리지가
100% 테스트가 되었다를 의미하지는 않는다
(예를 들면 다른 입력 값을 갖는 여러 테스트 케이스가 수행되어도 커버리지는
항상 같을 수 있다)
- 코드 커버리지를 측정 하는 경우는 오직 구현된 코드에 대해서만 측정되기 때문에
구현되지 않은 요건에 대해서는 측정되지 않는다
테스트 커버리지는 단순히 테스트(실행)가 얼마나 수행되었는지 보여주는 참조지표일 뿐,
테스트 자체가 잘 수행되었는지를 보장해 주지는 못한다
10. ClinicService 테스트 코드 작성
3. 등록 기능에 대한 테스트 작성 – 블랙박스
[애완동물 등록에 필요한 정보]
(1)이름
(2)생년월일
(3)타입(개/고양이/…)
(4)주인정보
(firstName, lastName,
address, city, telephone)
ClinicServiceImpl#savePet(Pet pet) Pet
개발 코드 살펴보기
11. ClinicService 테스트 코드 작성
3. 등록 기능에 대한 테스트 작성 – 블랙박스
@Test
@Transactional
public void testSavePet_기본() throws Exception {
Owner testOwner = this.clinicService.findOwnerById(testOwnerId);
assertNotNull(testOwner);
int found = testOwner.getPets().size();
Pet newPet = new Pet();
newPet.setName("my test pet");
Collection<PetType> types = this.clinicService.findPetTypes();
newPet.setType(EntityUtils.getById(types, PetType.class, 2));
newPet.setBirthDate(TestDataUtils.string2LocalDate("2018-12-01"));
testOwner.addPet(newPet);
assertNull(newPet.getId());
assertEquals(found+1, testOwner.getPets().size());
// assertThat(testOwner.getPets().size()).isEqualTo(found + 1);
this.clinicService.savePet(newPet);
this.clinicService.saveOwner(testOwner);
testOwner = this.clinicService.findOwnerById(6);
assertEquals(found+1, testOwner.getPets().size());
// checks that id has been generated
// assertNotNull(newPet.getId());
assertThat(newPet.getId()).isNotNull();
}
public class ClinicServiceTest extends AbstractPetStoreTestCase{
private final int testOwnerId = 1;
@Autowired
ClinicService clinicService;
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
import org.junit.runner.RunWith;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/business-config.xml"})
@ActiveProfiles("jdbc")
public abstract class AbstractPetStoreTestCase {
}
테스트 코드 작성1 – SavePet 기본 테스트
AbstractPetStoreTestCase
ClinicServiceTest 선언부
ClinicServiceTest#testSavePet_기본
12. ClinicService 테스트 코드 작성
3. 등록 기능에 대한 테스트 작성 – 블랙박스
@Test
@Transactional
public void testSavePet_JDBCTemplate이용() throws Exception {
int testOwnerId = jdbcTemplate.queryForObject("select id from owners LIMIT 1;",
Integer.class);
Owner testOwner = this.clinicService.findOwnerById(testOwnerId);
assertNotNull(testOwner);
int found = testOwner.getPets().size();
Pet newPet = new Pet();
newPet.setName("my test pet");
Collection<PetType> types = this.clinicService.findPetTypes();
newPet.setType(EntityUtils.getById(types, PetType.class, 2));
newPet.setBirthDate(TestDataUtils.string2LocalDate("2018-12-01"));
testOwner.addPet(newPet);
assertNull(newPet.getId());
assertEquals(found+1, testOwner.getPets().size());
// assertThat(testOwner.getPets().size()).isEqualTo(found + 1);
this.clinicService.savePet(newPet);
this.clinicService.saveOwner(testOwner);
testOwner = this.clinicService.findOwnerById(testOwnerId);
assertEquals(found+1, testOwner.getPets().size());
// checks that id has been generated
assertNotNull(newPet.getId());
// assertThat(pet.getId()).isNotNull();
}
public class ClinicServiceTest extends AbstractPetStoreTestCase{
private final int testOwnerId = 1;
@Autowired
ClinicService clinicService;
@Autowired
JdbcTemplate jdbcTemplate;
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
테스트 코드 작성2 – JdbcTemplate을 이용한 동적 데이터 세팅
13. ClinicService 테스트 코드 작성
3. 등록 기능에 대한 테스트 작성 – 블랙박스
@Test
public void testFindPetTypes_DB에애완동물종류가없는경우() throws
Exception {
when(mockPetRepositry.findPetTypes()).thenReturn(null);
Collection<PetType> result = clinicService.findPetTypes();
assertNull(result);
verify(mockPetRepositry, atLeastOnce()).findPetTypes();
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/business-config.xml"})
@ActiveProfiles("jdbc")
public class ClinicServiceMockTest extends AbstractPetStoreTestCase{
@Mock
PetRepository mockPetRepositry;
@Autowired
VetRepository vetRepository;
@Autowired
OwnerRepository ownerRepository;
@Autowired
VisitRepository visitRepository;
ClinicService clinicService;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockPetRepositry = mock(JdbcPetRepositoryImpl.class);
clinicService = new ClinicServiceImpl(mockPetRepositry, vetRepository,
ownerRepository, visitRepository);
}
테스트 코드 작성 3-1 – Mock을 이용하여 특정 상황을 만드는 테스트
테스트 대상을 순수 클래스(POJO)화 하여 테스트
14. ClinicService 테스트 코드 작성
3. 등록 기능에 대한 테스트 작성 – 블랙박스
@Test
public void testFindPetTypes_DB에애완동물종류가없는경우() throws
Exception {
when(mockPetRepositry.findPetTypes()).thenReturn(null);
Collection<PetType> result = clinicService.findPetTypes();
assertNull(result);
verify(mockPetRepositry, atLeastOnce()).findPetTypes();
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/mock-config-
clinicservice.xml", "classpath:spring/business-config.xml"})
@ActiveProfiles("jdbc")
public class ClinicServiceMockConfigTest extends AbstractPetStoreTestCase{
@Autowired()
PetRepository mockPetRepositry;
@Autowired
ClinicService clinicService;
@Before
public void setUp() throws Exception {
//MockitoAnnotations.initMocks(this);
}
테스트 코드 작성 3-2 – Mock을 이용하여 특정 상황을 만드는 테스트
<bean id="mockPetRepository" class="org.mockito.Mockito" factory-method="mock"
primary="true">
<constructor-arg value="org.springframework.samples.petclinic.repository.PetRepository"/>
</bean>
테스트 리소스 폴더에 임의의 설정 파일을 추가 정의
Mocking하려는 대상에 빈 설정을 덮어씌워서 Mocking
테스트 리소스 폴더에 정의한 테스트 config을 추가
15. 테스트 커버리지를 이용한 테스트 접근
(이클립스 플러그인 eclemma이용)
4. 등록기능에대한 테스트작성 – 화이트박스
Window>Preferences 에서 java>Code Coverage를 선택한다
개발코드에 대해서만 커버리지 측정을 하기 위해
Only path entries matchin에 “main” 을 입력한다
테스트 코드를 선택하고 Run As가 아닌 Coverage As로 테스트를 수행한다
사전 설정 테스트 실행(Coverage As)
※ EclEmma 플러그인 설치는 별도 자료 참조
16. 테스트 커버리지를 이용한 테스트 접근
4. 등록기능에대한 테스트작성 – 화이트박스
테스트 커버리지 결과 확인(Line Counter)
1) 테스트가 수행되며, 자동으로 Coverage 뷰가 표시된다
2) 라인 커버리지 확인을 위해 Line Counter를 선택한다
3) 상세 파일을 선택하면 에디터에 해당 코드레벨에서의 커버리지
가 색으로 표시된다(초록색:cover, 빨간색:uncover, 노란색:부분적
으로cover)
17. 테스트 커버리지를 이용한 테스트 접근
1) Branch Counter를 선택하면 분기조건에 대한 커버리
지를 확인할 수 있다
2) 코드로 보면 pet이 null인지 확인하는 분기에서 항상
false였기 때문에 분기문에 노란색, 실행 코드에 빨간색으
로 표시됨을 확인할 수 있다
Branch Counter로 브랜치 커버리지(*컨디션 커버리지) 확인
4. 등록기능에대한 테스트작성 – 화이트박스
18. 4. 등록기능에대한 테스트작성 – 화이트박스
테스트 커버리지를 이용한 테스트 접근
분기문에 대한 테스트 추가 후 커버리지 확인
@Test(expected=PetClinicCoreException.class)
public void testSavePet_pet이Null인경우() throws Exception {
Pet newPet = null;
this.clinicService.savePet(newPet);
fail("기대한 Exception이 발생하지 않았습니다");
}
※ pet이Null인경우에 대한 테스트 추가
2) Coverage As로 테스트를 수행하고 나면 코드 상의 노란색
/빨간색 부분이 초록색으로 바뀌는 것을 확인할 수 있다
19. 4. 등록기능에대한 테스트작성 – 화이트박스
※ Exception 발생에 대한 테스트
@Test(expected=PetClinicCoreException.class)
public void testSavePet_pet이Null인경우() throws Exception {
Pet newPet = null;
this.clinicService.savePet(newPet);
fail("기대한 Exception이 발생하지 않았습니다");
}
업무 로직상 정합하지 않는 상황에 대해 개발코드에서는
미리 정의한 임의의 Exception을 발생시킬 수 있다
“안 그래도 많이 발생하는 에러를 왜 일부러 또 발생시키나욧!!??”
※ 하위 레벨에서 사전에 정의한 기준에 따라 (임의의) Exception을
발생시키면 상위레벨에서 이를 상황별로 비즈니스 요건에 따라
다르게 처리할 수 있는 기회가 생긴다
JUnit4에서 특정 Exception이 발생해야지만
테스트가 성공하도록 하는 코드
Exception이 발생하면 fail문이 실행되지 않음
20. 4. 등록기능에대한 테스트작성 – 화이트박스
과정 회고
좋았던 점
아쉬운 점/
개선할 점
요건(개발코드?)을 분석하고 무엇을 테스트할지 고민하기
Controller/Service/Repository 중 어떤 영역을 테스트할지 고민하기
코드 레벨에서의 단위 / 통합 테스트를 이해하고 고민해 보기
교육 목표
기본적인 테스트 코드 작성
Mock을 이용하여 특정 상황을 만들고 테스트하기
테스트 코드 안에서 동적 쿼리 실행하기
라인/브랜치 커버리지를 측정해 보고 테스트 보완하기 얘기해 보아요~