33
생존 프로그래밍:TEST OKJSP 13주년 컨퍼런스, 2013-12-07 최범균([email protected])

Okjsp 13주년 발표자료: 생존 프로그래밍 Test

Embed Size (px)

DESCRIPTION

okjsp 13주년 행사에서 발표한 자료입니다.

Citation preview

Page 1: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

생존 프로그래밍:TEST OKJSP 13주년 컨퍼런스, 2013-12-07

최범균([email protected])

Page 2: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

목표

● 목표임○ 테스트 코드를 만들지 않는 프로그래머들에게, 어쩐지 테스트 코드가 개발 시간을 줄여줄 것 만 같은 “느낌”을 주는 것

● 목표 아님○ JUnit 설명하기○ 기타 프레임워크 설명하기○ TDD 설명하기

Page 3: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

● 사람이 하는 TEST● 테스트 시간을 단축하려면● 기계가 하는 테스트

○ 스프링 통합 테스트○ 한 클래스 / Mock

● 주의 사항○ 테스트 환경 맞추기

● 정리

내용

Page 4: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

구현 과정?

Java

XML

SQL

HTML

JS

WAS

기능과 관련된 모든 코드 작성

WAS실행

웹브라우저 테스트

버그 또는에러

원인분석 및코드수정

Page 5: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

WAS

디버깅 과정?

Java XML SQLHTML JS

<input> 태그의

name이 잘못됐나

?커맨드 객체에

값이 안 들어가나?

쿼리에서

컬럼이

빠졌나?

Ajax 호출할 때값을 안 주나?

설정이

올바른가

?

재시작안했나?

왜 DB에 데이터가 null이지?

Page 6: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

개발 시간의 증가 원인

WAS실행

웹브라우저 테스트

버그 또는에러

원인분석 및코드수정

XMLSQLHTMLJSWASJava

한 번에 집중해서만들어야 할 게 많음!시간이 오래 걸리고,흐름 깨지기 쉬움

확인해 봐야 할 게 많음!한 곳만 잘못되지 않음!오래 걸림, 흐름 깨지기 쉬움

폼에 수동 입력 (주로 ‘1’, ‘a’ 등 입력)URL을 붙여넣기 하기도 함

JRebel 없어재시작해야 함심한 경우, 재배포

Page 7: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

개발/디버깅/테스트 시간을 줄이려면

범위 좁히기(즉, 나누기)

Page 8: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

한 놈씩 골라서! 관련된 것만 묶어서!

Java XMLSQL+DB

HTML JS WAS

Controller SVC DAO

DAO 잘 되나?

스프링 설정문제없나?

컨트롤러가 응답을 정상으로 만드나?

서버 기능은 정상동작하나?

다 잘 되나?

Page 9: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

테스트 종류

Java XMLSQL+DB

HTML JS WAS

Controller SVC DAO

단위테스트단위테스트

end-to-end (통합) 테스트

Acceptance 테스트

통합 테스트

Page 10: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

JUnit

● 테스트 프레임워크○ 코드로 테스트 시나리오 작성○ 코드로 결과 검증

● 쉬운 반복 실행○ 지정한 범위의 테스트를 빠르게 실행 가능○ IDE는 모두 JUnit 실행 기능 기본 제공

● 주로 다루는 범위○ 한 개 클래스 (단위 테스트)○ 서버 기능 (통합 테스트)○ 종종, end-to-end 테스트 (통합 테스트)

Page 11: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

자바 웹 서버 개발 속도에 도움 되는 것

● 단위 테스트○ 한 모듈(보통 클래스 한 개)의 기능을 테스트○ 예,

■ 컨트롤러가 JSON을 제대로 만드나?■ 엑셀 생성 모듈이 엑셀 파일을 제대로 만드나?■ DAO가 데이터를 잘 읽어오나?■ 스프링 설정이 올바른가?

● 서버측 통합 테스트○ 모듈을 통합해서 수행하는 기능 테스트○ 예,

■ 컨트롤러-서비스-DAO-DB 연동된 상태의 기능 테스트

Page 12: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

테스트 작성 사례

● 기능○ DB에서 대량의 데이터를 읽어와 서버의 로컬에 엑셀 파일을 생성한 후, 그 파일을 웹 브라우저에 내려주는 기능

Page 13: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

시작

● 일단 막 구현

검색해서 iBATIS의 데이터를 순차적으로 받아 엑셀로 만드는 코드 찾음

복사/붙여넣기 해서 코드 작성

토드에서 쿼리 돌아감

다른 것 참조해서 스프링 설정

톰캣 실행/브라우저 실행/링크 클릭

결과는…..

나 초짜

Page 14: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

잔뜩 코딩한 뒤 실행한 결과는

Page 15: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

테스트 방법

복사&붙여넣기버튼 클릭넣기

에러 발생

뭔가 코드 수정(서버 재시작..)

여러 코드 탐색

반복

Page 16: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

범위 좁히기: 에러 나는 영역

에러 유발자!

Page 17: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

에러 잡기 위한 테스트 코드 작성@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration( locations = { "classpath:spring-*.xml" })public class BaseBigExcelDownDAOIntTest { @Autowired private BaseBigExcelDownDAO excelDownDao; private String tempFilePath = "....";

@Before public void setUp() { deleteIfTempFileExists(); }

private CustomerServiceRequestCommand createCommand() { CustomerServiceRequestCommand command = new CustomerServiceRequestCommand(); command.setMenuId("C000732"); command.setCust_flag("0"); command.setReceive_num_flag("2"); ... return command; }

@Test public void shouldCreateExcelFile() { String queryId = "..."; excelDownDao.createSheetFile( querId, createCommand(), createSheetMap(), tempFilePath); assertTempFileCreated(); }

private void assertTempFileCreated() { File file = new File(tempFilePath); assertThat(file.exists(), equalTo(true)); }

…. ….}

수동 입력과눈 확인 대체

Page 18: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

테스트 과정

테스트 및 결과 확인

에러 확인

원인 확인

Page 19: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

테스트 과정

속도감수동 입력/서버/브라우저

필요 없음

집중

Page 20: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

범위 좁히기: 파일 전송파일 전송 기능 구현 전

Page 21: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

컨트롤러 테스트

실제 DB 연동/엑셀 파일 생성 필요 없음

컨트롤러는 다른 객체가 생성한파일을 response로 전송해주면 됨데이터를 어디서 읽어오는지는중요하지 않음!

Page 22: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

진짜 대신 가짜 쓰기public class CustomerServiceRequestControllerTest { private CustomerServiceRequestController controller = new CustomerServiceRequestController(); @Mock private BaseBigExcelDownDAO mockBaseBigExcelDownDAO; private MockHttpServletResponse mockResponse = new MockHttpServletResponse(); private CustomerServiceRequestCommand command = new Customer…(); private byte[] realBytes = new byte[125];

@Before public void setUp() { controller.setBaseBigExcelDownDAO(mockBaseBigExcelDownDAO);

for (byte i = 0; i < realBytes.length; i++) realBytes[i] = i; doAnswer(new Answer<Void>() { public Void answer(InvocationOnMock invocation) throws Throwable { String genFilePath = (String) invocation.getArguments()[3]; FileCopyUtils.copy(realBytes, new File(genFilePath)); return null; } }).when(mockBaseBigExcelDownDAO).createSheetFile(anyString(), any(), any(LinkedHashMap.class), anyString()); }

가짜 쓰도록 설정

가짜에서 컨트롤러가 사용할 파일 생성

Page 23: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

진짜 대신 가짜 쓰기public class CustomerServiceRequestControllerTest { private CustomerServiceRequestController controller = new CustomerServiceRequestController(); @Mock private BaseBigExcelDownDAO mockBaseBigExcelDownDAO; private MockHttpServletResponse mockResponse = new MockHttpServletResponse(); private CustomerServiceRequestCommand command = new Customer…(); private byte[] realBytes = new byte[125];

@Before public void setUp() { controller.setBaseBigExcelDownDAO(mockBaseBigExcelDownDAO); … } @Test public void should_create_filepath_in_random_and_send_filedata_to_client() { controller.customerServiceRequestExcel(mockResponse, command); assertSendDataBytes(); }

private void assertSendDataBytes() { byte[] sendedFileBytes = mockResponse.getContentAsByteArray(); assertEquals(realBytes.length, sendedFileBytes.length); for (int i = 0; i < realBytes.length; i++) assertEquals(realBytes[i], sendedFileBytes[i]); }

가짜 사용

컨트롤러가 전송한 파일 데이터 확인

Page 24: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

public class CustomerServiceRequestControllerTest { private CustomerServiceRequestController controller = new CustomerServiceRequestController(); @Mock private BaseBigExcelDownDAO mockBaseBigExcelDownDAO; private MockHttpServletResponse mockResponse = new MockHttpServletResponse(); private CustomerServiceRequestCommand command = new Customer…(); private byte[] realBytes = new byte[125];

@Before public void setUp() { controller.setBaseBigExcelDownDAO(mockBaseBigExcelDownDAO); … } @Test public void should_create_filepath_in_random_and_send_filedata_to_client() { controller.customerServiceRequestExcel(mockResponse, command); assertSendDataBytes(); }

private void assertSendDataBytes() { byte[] sendedFileBytes = mockResponse.getContentAsByteArray(); assertEquals(realBytes.length, sendedFileBytes.length); for (int i = 0; i < realBytes.length; i++) assertEquals(realBytes[i], sendedFileBytes[i]); }

진짜 대신 가짜 쓰기

스프링 초기화 없음

DB 연동 없음

WAS/브라우저 없음

속도감

Page 25: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

가짜를 이용한 병행 개발개발자1: 가짜를 이용해서 개발 가능기능 검증은 단위 테스트 코드로

개발자2: 기능 구현기능 검증은 통합 테스트 코드로

Page 26: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

마지막: 톰캣 띄우고 기능 테스트상당한 부분의 코드를 테스트 코드로 확인한 뒤에,

거의 마지막에 기능 테스트 수행

이 시점에서 오류는 대부분 UI 관련된 것서버쪽은 이미 테스트가 거의 끝남

Page 27: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

사실은, 사용자 기능도 코드로 테스트

● 테스트 코드가 웹 브라우저를 실행해서 입력/ 검증 수행

● 관련 프레임워크○ Cucumber○ FitNess○ Selenium○ Watir○ Geb○ …

Page 28: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

테스트 작성 시 주의 사항

● 동일 조건에서 테스트가 동작하도록○ 즉, 테스트 코드를 반복해서 실행해도 문제 없도록

● 예,○ 파일 생성 테스트면, 기능 실행 전에 기존에 생성된 파일 삭제 처리

○ DB 조회 테스트면, 동일한 데이터 집합을 갖도록 처리

Page 29: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

정리

● 테스트 코드와 개발 시간 단축○ 사람 대신 기계가 테스트를 수행해서, 실행 시간 단축 효과

○ 개발 범위를 좁혀주어, 구현 시간 단축 효과○ 병행 개발 가능, 대기 시간 단축 효과

● 코딩+테스트+디버깅 시간○ 범위 나눠 테스트 <<< 모든 코드 구현 후 테스트

■ 모든 코드 구현: 코딩+테스트+디버깅+큰뻘짓■ 범위 나눠 구현: 코딩+테스트+디버깅+작은뻘짓

● 회귀(regression) 테스트로도 사용● 좋은 설계도 원하면: TDD 시도 필요

Page 30: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

참고

● 테스트 관련○ JUnit

■ 테스트 프레임워크○ 스프링 MVC 테스트

■ 스프링 컨트롤러에 대한 테스트 진행○ DBUnit

■ DB 데이터 초기화/확인 기능 제공■ spring-test-dbunit (편리성 추가)

○ Mockito 등■ 가짜 객체 생성 및 호출 여부 검증 기능 제공

Page 31: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

참고: Spring MVC 응답 테스트 예시public class PaymentSyncSourceListControllerTest { private MockMvc mockMvc; private PaymentSyncSourceListController controller = new PaymentSyncSourceListController();

@Before public void setUp() { mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); }

@Test public void jsonList() throws Exception { mockMvc.perform(get("/sync/syncsources.json").param("offset", "0").param("size", "10")) .andExpect(status().isOk()) .andExpect(content().contentType(createJsonMediaType())) .andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$[0].id", is(2))) .andExpect(jsonPath("$[0].totalAmount", is(50000))) ... .andExpect(jsonPath("$[0].orderLines", hasSize(1))) .andExpect(jsonPath("$[0].orderLines[0].id", is(2))) ... .andExpect(jsonPath("$[1].deliveryCharge", is(2500))); }

Page 32: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

맺음말

일단 한 번 테스트 코드 만들기!

Page 33: Okjsp 13주년 발표자료: 생존 프로그래밍 Test

Q&A, 의견트위터: @madvirus

이메일: [email protected]