--- tags: test, tdd --- TDD 세미나 3부 - Spring 에서의 TDD === # 3부. Spring 에서의 TDD ## 1. Spring이 뭔지 모르는 Junit 1부에서 간단히 얘기했지만 Spring 은 기본적으로 ApplicationContext (이하 AC) 에서 객체들을 꺼내서 쓴다. > 정확히는 AC 의 Bean Factory 에서. ConsultDAO 타입의 Spring Bean 이 필요하다면, @Autowired 로 주입받아 쓰면 된다. ```java public class ConsultDAOTest { @Autowired ConsultDAO dao; // null @Test public void 등록테스트(){ dao.insert(some); } } ``` 그런데 Junit 에서 이런 코드를 테스트를 하려면 문제가 있다. 테스트 클래스에는 어디에도 AC 관련 설정이 없기 때문에 @Autowired 는 그냥 아무 일도 할수없는 주석에 불과하다. 따라서 dao 에는 아무런 빈도 주입되지 않아 이 테스트는 NPE (NullPointException) 발생하며 종료된다. ### JUnit이 Spring 을 알게해주자 ```java @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = RootConfig.class) public class ConsultDAOTest { } ``` #### @RunWith(SpringJUnit4ClassRunner.class) 추가 SpringJUnit4ClassRunner 는 일종의 테스트러너다. AC 를 공통으로 쓸수있게 해주기도 한다. #### @ContextConfiguration(classes = RootConfig.class) 추가 AC 로드에 참조할 Configuration 파일을 지정할수 있다. (classes 또는 locations) RootConfig 는 기존의 application-context.xml 을 JavaConfig 스타일로 구현한거다. xml도 넣을수 있긴 하다. #### @RunWith, @ContxtConfiguration 설명은 아래 참고 > https://bonjugi.github.io/Spring-TEST/ ## 2. test suite 예시코드 설명 아래는 간단한 DAO 테스트 이다. ```java @Transactional("transactionManager") @Slf4j @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = RootConfig.class) public class ConsultDAOTest { @Autowired private ConsultDAO dao; private Consult fixtureConsult; @Before public void 등록테스트(){ // given Consult newConsult = new Consult(); newConsult.setCellPhone("01077771111"); newConsult.setConsultStatusCode(ConsultStatus.APPLY); newConsult.setConsultType(ConsultType.PARTNER); newConsult.setContent("귀하의 솔루션이 너무마음에 듭니다. 연락주십시오"); newConsult.setCreateTime(LocalDateTime.now()); newConsult.setEmail("dev.test@gmail.com"); newConsult.setRealName("스티브잡스"); // when dao.insert(newConsult); // then assertThat(newConsult.getConsultId()).isNotNull(); fixtureConsult = newConsult; } @Test public void 아이디_동치성테스트(){ Consult consult = new Consult(); consult.setConsultId(1); Consult same = new Consult(); same.setConsultId(1); Consult different = new Consult(); different.setConsultId(2); assertThat(consult).isEqualTo(same); assertThat(consult).isNotEqualTo(different); } @Test public void 조회테스트(){ Consult selected = dao.getById(fixtureConsult.getConsultId()); assertThat(selected).isEqualTo(fixtureConsult); assertThat(selected.getContent()).isEqualTo(fixtureConsult.getContent()); assertThat(selected.getCellPhone()).isEqualTo(fixtureConsult.getCellPhone()); assertThat(selected.getEmail()).isEqualTo(fixtureConsult.getEmail()); assertThat(selected.getConsultType()).isEqualTo(fixtureConsult.getConsultType()); assertThat(selected.getConsultStatusCode()).isEqualTo(fixtureConsult.getConsultStatusCode()); } @Test public void 삭제테스트(){ int i = dao.deleteById(fixtureConsult.getConsultId()); assertThat(i).isEqualTo(1); } } ``` #### @Transactional JUnit에서의 @Transactional은 테스트 수행중의 모든 트랜잭션을 롤백해준다. #### @Before JUnit 은 메소드별로 각각의 인스턴스가 되어 동작한다. (각 테스트별로 간섭을 최소화 하고자 하는 JUnit 창시자의 의지) 때문에 특정 테스트를 수행하기 전 FIXTURE 가 필요하다면 @Before로 미리 셋팅해 둘 수 있다. #### FixtureConsult 조회 테스트시 fixtureConsult 를 의존하고 있다. DB 를 의존하면 매번 값이 달라지기 때문에 @Before를 통해 갓 생성된 consult 를 의존하여 깨지지 않는 테스트를 만들었다 #### 등록테스트 dao.inset 메소드를 테스트한다. dao.inset 이후에는 consultId가 생성이 되는지 (AI) `assertThat(newConsult.getConsultId()).isNotNull();` 테스트를 수행 했다. 몇번이 할당되는지는 알수 없어 isNotNull 로 체크한다. #### 아이디_동치성테스트 Consult는 식별자가 존재한다. (pk) 식별자가 존재하는 Entity는 식별자만으로 동치성 체크를 할수 있게 Equals HashCode를 재정의 해두면 좋다. 식별자 만으로 동치성 체크를 하게 하면 장점이 있는데 아래서 설명 하겠다. #### 조회테스트 등록테스트에서 생성된 fixture 를 이용하여 조회해 본 후, fixture와 조회된 객체의 모든 속성들을 비교 해 본다. 중요한 도메인 일 수록 모든 속성을 비교하는게 좋다. #### 삭제테스트 @Before 등록테스트 에서 만들어진 consultId 로 deleteById 를 수행했다. 마이바티스 특성상 삭제시 리턴값이 1이 발생한다. 삭제가 안됐었다면 0을 리턴하여 테스트는 성공하지 못했을 것이다. 사실 DAO 같은 인프라스트럭처는 등록->조회->수정->조회->삭제 를 미리 만들어 둬도 좋다. 필요하지 않은것은 만들지 않는게 원칙이기는 하나,대부분 언젠가는 필요해지기 때문이기도 하고, 각가의 api를 조합되어 서로간의 커버리지가 생기기 때문에 만들어두면 좋다. 마지막의 삭제 테스트도 단순히 1을 리턴받았는지가 아니라, 삭제한 consultId 로 다시 조회를 해본 뒤 isNull() 체크를 할수 있게 되어, 보기 좋은 테스트 스윗이 될 것이다. > 물론 DAO는 단위테스트가 중요하지 않다는 말은 아니다. > 통계나 다이나믹쿼리가 있는 어려운 DAO 일수록 단위테스트도 열심히 커버 해야 할 것이다. ## 부록1 식별자 동치성 체크 Consult 코드를 보면 EqualsAndHashCode 가 lombok 을 이용하여 오버라이드 되어있다. ```java @EqualsAndHashCode(of = "consultId") @Data public class Consult { private Integer consultId; private ConsultType consultType; @NotBlank(message = "이메일을 작성해 주세요.") @Email(message = "이메일 형식을 확인해 주세요.") private String email; @NotBlank(message = "이름을 작성해 주세요.") private String realName; @NotBlank(message = "내용을 작성해 주세요.") private String content; @NotBlank(message = "전화번호를 작성해 주세요.") @Pattern(regexp = "[0-9]{10,11}", message = "10~11자리의 숫자만 입력가능합니다") private String cellPhone; private ConsultStatus consultStatusCode; private LocalDateTime createTime; } ``` 이것은 2개의 Consult 인스턴스의 equlas 비교시 아이디만 체크하겠다는 의미인데, 아래와 같은 장점들이 있다. 1. 자바 스트림 간편 2. contains 등 간편화 3. DB 에서 조회된 값 동일성 간편 4. 실제로 다른 필드는들 속성(Attribute) 이고, 식별은 아이디로만 한다. 즉 이름은 달라도 DB상에서 같은 레코드 이다. (작성중...) --- ## 부록2 - 2가지 TDD 방식. Classist vs Mockist DAO 가 아니라 Service 는 어떻게 테스트 하나? DAO 테스트를 모두 포함할텐데 이것들의 test suite를 반복해야하나? 아니다. mock 으로 항상 일정하게 dao.select() 의 리턴값을 조작할수 있다. Mockito 라는 프레임워크를 쓰면 되는데 너무 길어지니 패스. 이러한 mock 주입방식을 선호하는사람을 Mockist 라고 한다. 반대로 Service 테스트를 만들다 보면 DAO에서 했던 테스트와 중복되는 것을 제거해 나가면서 클래식하게 TDD 할수도 있다. 이것을 Classist 라고 한다. ### Classist? Mock 없이 순수 코드에 의해 점진적으로 만드는 방식을 선호하는 타입 켄트백 마틴파울러 등 창시자들은 주로 Classist 이다. Mock, Stub 등을 쓰지 않아 단위테스트가 어려우나, 객체간의 교류에 의한 이상도 잘 커버할수 있음. ### Mockist? Mock 등을 이용해서 단위 테스트간의 간섭을 최대한 줄이는 방식을 선호하는 타입 단위테스트가 쉬우며 기존의 테스트가 없는 스파게티 코드에 접근하기 좋음. ### 결론 두가지 타입중 한가지만 써야한다는 그런건 없다. 단지 사람이 추구하는 방식이 다를뿐이다. 적절히 섞어 쓰면 된다.