본문 바로가기

Spring/토비의 스프링 3.1

6.2 고립된 단위 테스트


가장 좋은 테스트 방법 : 가능한 작은 단위로 쪼개서 테스트

  • 테스트 실패 시 원인을 찾기가 쉽기 때문
  • 불가능한 경우도 있음 : 다른 오브젝트와 환경에 의존하고 있는 경우

6.2.1 복잡한 의존관계 속의 테스트

UserService

  • 매우 간단한 기능만 가지고 있음
  • 그럼에도 불구하고 세 가지 타입의 의존 오브젝트가 필요
    1. UserDao : DB 처리
    2. MailSender : 메일 처리
    3. PlatformTransactionManager : 트랜잭션 처리

UserServiceTest

  • 본래의 목적 : UserService에 대한 테스트
  • 세 가지 의존관계를 갖고 있으므로 테스트가 진행되는 동안 세 가지 오브젝트가 모두 정상이어야 한다.
  • 실제로는 해당 오브젝트의 뒤에 존재하는 훨씬 더 많은 오브젝트와 환경, 서비스, 서버 등을 함께 테스트하는 것

6.2.2 테스트 대상 오브젝트 고립시키기

테스트의 대상이 외부 환경에 영향을 받지 않으려면?

  • 테스트를 고립시켜야 한다.
  • 테스트 대역을 사용

테스트를 위한 UserServiceImpl 고립

PlatformTransactionManager

  • 트랜잭션 코드를 독립시켜 UserServiceImpl은 더 이상 PlatformTransactionManager에 의존하지 않음

UserDao

  • 부가적인 검증 기능을 가진 목 오브젝트

UserServiceImpl

  • UserServiceImpl 기능이 수행되더라도 그 결과가 DB 등을 통해 남지 않으므로, 기존의 작업 방법으로 검증하기 힘들다
  • UserServiceImpl과 UserDao에게 어떤 요청을 했는지에 대한 확인 작업 필요
  • DB에 반영되지는 않지만 UserDao의 update() 메소드를 호출한 것을 확인하면 DB에 반영될 것이라 예상 가능(Dao는 이미 테스트 완료되었으므로)

고립된 단위 테스트 활용

기존의 upgradeLvls()의 테스트 구성

  1. DB 테스트 데이터 준비
    • DB에 직접 정보를 넣어주어야 함
  2. 메일 발송 여부 확인을 위한 목 오브젝트 DI
  3. 테스트 대상 실행
  4. DB에 저장된 결과 확인
    • DB에서 데이터를 가져와 결과를 확인
  5. 목 오브젝트를 이용한 결과 확인

UserDao 목 오브젝트

1.번과 4.번 과정의 경우 DB에 직접 연결해야함

  • 목 오브젝트를 이용한 테스트 적용
  • UserDao와 어떠 정보를 주고받는지 입출력 내역을 확인해야함

UserServiceImpl이 UserDao를 사용하는 경우

public void upgradeLvls() {
    // 업그레이드 후보 사용자 목록을 가져옴
    List<User> users = userDao.getAll();

    for (User user : users) {
        if (canUpgradeLvl(user)) {
            upgradeLvl(user);
        }
    }
}

protected void upgradeLvl(User user) {
    user.upgradeLvl();
    // 수정된 사용자를 DB에 반영한다.
    userDao.update(user);
    sendUpgradeEMail(user);
}

userDao.getAll()

  • 레벨 업그레이드 후보가 될 사용자 목록을 가져옴
  • 미리 준비된 사용자 목록을 제공해주어야 함

userDao.update(user)

  • 리턴 값이 없음
  • UserDao가 미리 준비해 둘 것은 없음
  • 그러나 '변경'에 해당하는 부분을 검증할 수 있는 중요한 기능

MockUserDao

  • getAll()은 스텁으로서, update()는 목 오브젝트로서 UserDao 타입 테스트 대역 필요
  • UserServiceTest 전용이므로 스태틱 내부 클래스로 정의
public class UserServiceTest {
    /**
     * UserDao 테스트 대역
     */
    static class MockUserDao implements UserDao {
        private List<User> users;
        private List<User> updated = new ArrayList<User>();

        private MockUserDao() {
        }

        private MockUserDao(List<User> users) {
            this.users = users;
        }

        public List<User> getUpdated() {
            return this.updated;
        }

        // Stub
        @Override
        public List<User> getAll() {
            return this.users;
        }

        // Mock Object
        @Override
        public void update(User user) {
            updated.add(user);
        }

        @Override
        public void add(User user) {
            throw new UnsupportedOperationException();
        }

        @Override
        public User get(String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void deleteAll() {
            throw new UnsupportedOperationException();
        }

        @Override
        public int getCount() {
            throw new UnsupportedOperationException();
        }
    }
}

MockUserDao

  • UserDao 인터페이스 구현
  • 사용하지 않는 메소드 역시 모두 구현해주어야 하며, UnsupportedOperationException을 던진다.
  • 두 개의 User 타입 리스트를 정의
    1. 생성자를 통해 전달받은 사용자 목록을 저장 후, getAll()에 돌려줌; DB와 통신하지 않음
    2. update() 메소드 실행 시 업그레이드 대상 User 오브젝트를 저장해뒀다가 검증을 위해 돌려줌

UserServiceTest

public class UserServiceTest {
    @Test
    public void upgradeLvls() throws Exception {
        // 고립된 테스트에서는 테스트 대상 오브젝트를 직접 생성하면 된다.
        UserServiceImpl userServiceImpl = new UserServiceImpl();

        // 목 오브젝트로 만든 UserDao를 직접 DI gownsek.
        MockUserDao mockUserDao = new MockUserDao(this.users);
        userServiceImpl.setUserDao(mockUserDao);

        MockMailSender mockMailSender = new MockMailSender();
        userServiceImpl.setMailSender(mockMailSender);

        userServiceImpl.upgradeLvls();

        // MockUserDao로부터 업데이트 결과를 가져온다.
        // 업데이트 횟수와 정보를 확인한다.
        List<User> updated = mockUserDao.getUpdated();
        assertThat(updated.size(), is(2));
        checkUserAndLvl(updated.get(0), "joytouch", Lvl.SILVER);
        checkUserAndLvl(updated.get(1), "madnite1", Lvl.GOLD);

        List<String> request = mockMailSender.getRequests();
        assertThat(request.size(), is(2));
        assertThat(request.get(0), is(users.get(1).getEmail()));
        assertThat(request.get(1), is(users.get(3).getEmail()));
    }

    /*
    ** id와 lvl을 확인하는 헬퍼 메서드
    */
    private void checkUserAndLvl(User updated, String expectedId, Lvl expectedLvl) {
        assertThat(updated.getId(), is(expectedId));
        assertThat(updated.getLvl(), is(expectedLvl));
    }
}

고립된 테스트

  • 스프링 컨테이너에서 빈을 가져올 필요가 없다.
  • UserServiceImpl 오브젝트를 직접 생성
  • @RunWith를 제거할 수 있음(upgradeLvls() 테스트만 있다면)
  • MockUserDao를 사용하도록 수동 DI
  • UserDao의 update()를 이용해 몇 명의 사용자 정보를 DB에 수정하려고 했는지, 그 사용자는 누구인지, 어떤 레벨로 변경되었는지를 확인

테스트 수행 성능의 향상

테스트 수행시간이 이전보다 빨라짐

  • 직접적으로 필요하지 않은 의존 오브젝트와 서비스를 모두 제거했기 때문
  • 테스트 수행시간 : 반복적인 테스트를 위핸 필수적

6.2.3 단위 테스트와 통합 테스트

단위 테스트
테스트 대역을 이용해 의존 오브젝트나 외부 리소스를 사용하지 않도록 고립시켜서 하는 테스트
통합 테스트
두 개 이상의 성격이나 계층이 다른 오브젝트를 연동하거나 외부의 DB, 파일 또는 서비스 등의 리소스가 참여하는 테스트

단위테스트 vs 통합테스트

  • 항상 단위 테스트를 먼저 고려
  • 스텁이나 목 오브젝트 등의 테스트 대역을 이용하도록 테스트를 만든다.
  • 외부 리소스를 사용해야만 가능한 테스트는 통합테스트로 만든다.
  • 대표적 통합테스트 : DAO는 DB를 통해 로직을 수행하는 인터페이스와 같은 역할
  • DAO 테스트
    1. 외부 리소스를 사용하는 통합 테스트
    2. 하나의 기능 단위를 테스트
  • 여러 개의 단위가 의존관계를 가지고 동작할 경우 통합 테스트 필요
  • 단위 테스트를 만들기가 너무 복잡하다고 생각되는 테스트
  • 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합테스트
  • 통합 테스트를 해야하는 경우에도 가능한 많은 부분을 미리 단위 테스트로 검증해두는 것이 유용하다.

테스트하기 편한 코드가 깔끔하고 좋은 코드가 가능성이 높다.

6.2.4 목 프레임워크

단위 테스트의 단점

  • 장점은 많지만 작성이 번거로움
  • 목 오브젝트를 만들 때 사용하지 않는 메소드까지 모두 구현해주어야 함

Mockito 프레임워크

목 클래스를 일일이 준비해둘 필요가 없음

  • 스태틱 메소드를 한 번 호출해주면 만들어짐
UserDao mockUserDao = mock(UserDao.class);

이렇게 만들어진 목 오브젝트는 아직 아무런 기능이 없다.

  • 스텁 기능을 추가해야함
  • mockUserDao.getAll()이 호출되었을 때(when), users를 리턴해주라(thenReturn)
when(mockUserDao.getAll()).thenReturn(this.users);

update() 메소드가 두 번 호출됐는지 확인하는 방법 + User 타입의 오브젝트를 파라미터로 받으며 update() 메소드가 두 번 호출됐는지(times(2))를 확인(verify)

verify(mockUserDao, times(2)).update(any(User.class));

Mockito 목 오브젝트 사용법

  1. 인터페이스를 이용해 목 오브젝트 생성
  2. 리턴할 값이 있으면 지정
  3. 테스트 대상 오브젝트에 DI
  4. 검증

upgradeLvls()

public class UserServiceTest {
    @Test
    public void mockUpgradeLvls() throws Exception {
        UserServiceImpl userServiceImpl = new UserServiceImpl();

        UserDao mockUserDao = mock(UserDao.class);
        when(mockUserDao.getAll()).thenReturn(this.users);
        userServiceImpl.setUserDao(mockUserDao);

        MailSender mockMailSender = mock(MailSender.class);
        userServiceImpl.setMailSender(mockMailSender);

        userServiceImpl.upgradeLvls();

        verify(mockUserDao, times(2)).update(any(User.class));
        verify(mockUserDao, times(2)).update(any(User.class));
        verify(mockUserDao).update(users.get(1));
        assertThat(users.get(1).getLvl(), is(Lvl.SILVER));
        verify(mockUserDao).update(users.get(3));
        assertThat(users.get(3).getLvl(), is(Lvl.GOLD));

        ArgumentCaptor<SimpleMailMessage> mailMessageArg = ArgumentCaptor.forClass(SimpleMailMessage.class);
        verify(mockMailSender, times(2)).send(mailMessageArg.capture());
        List<SimpleMailMessage> mailMessages = mailMessageArg.getAllValues();
        assertThat(mailMessages.get(0).getTo()[0], is(users.get(1).getEmail()));
        assertThat(mailMessages.get(1).getTo()[0], is(users.get(3).getEmail()));
    }
}

목 오브젝트가 어떻게 호출되었는지 검증

  • times() : 메소드 호출 횟수 검증
  • any() : 파라미터의 내용을 무시
  • 레벨의 변화는 assertThat으로 직접 확인


' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

6.4 스프링의 프록시 팩토리 빈  (0) 2019.02.13
6.3 다이내믹 프록시와 팩토리 빈  (0) 2019.02.13
6.1 트랜잭션 코드의 분리  (0) 2019.02.07
6장. AOP  (0) 2019.02.07
5.5 정리  (0) 2019.02.07