Unit Test 작성시 필요한 여러 기술 (Test double)

JINHO LEE·2023년 2월 6일
0

Unit Test 작성시 필요한 여러 기술

Test Double 이란

  • 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체를 의미한다.
  • Test Double은 크게 Dummy, Fake, Stub, Spy, Mock으로 나눈다.

사진 출처) https://subscription.packtpub.com/book/application-development/9781783983605/1/ch01lvl1sec09/understanding-test-doubles

1. Dummy

  • 가장 기본적인 Test Double이다.
  • 인스턴스 객체가 필요하지만 기능은 필요하지 않는 경우에 사용한다.
  • Dummy 객체의 메서드가 호출되었을 때, 정상 동작은 보장하지 않는다.
  • 객체는 전달되지만 사용되지 않는 객체이다.
public interface PrintTest {
    void print();
}
public class PrintTestgDummy implements PrintTest {
    @Override
    public void print() {
        // 아무런 동작을 하지 않는다.
    }
}

실제 객체는 printTest 인터페이스의 구현체를 필요하지만, 특정 테스트에서는 해당 구현체의 동작이 필요하지 않을 수 있다.

이처럼 동작하지 않아도 테스트에 영향을 미치지 않는 객체를 Dummy 객체라고 한다.

2. Fake

  • 복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체이다.
  • 동작의 구현을 가지고 있지만, 실제 프로덕션에는 적합하지 않은 객체이다.
@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    protected User() {}
    
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public Long getId() {
        return this.id;
    }
    
    public String getName() {
        return this.name;
    }
}
public interface UserRepository {
    void save(User user);
    User findById(long id);
}
public class FakeUserRepository implements UserRepository {
    private Collection<User> users = new ArrayList<>();
    
    @Override
    public void save(User user) {
        if (findById(user.getId()) == null) {
            user.add(user);
        }
    }
    
    @Override
    public User findById(long id) {
        for (User user : users) {
            if (user.getId() == id) {
                return user;
            }
        }
        return null;
    }
}

테스트해야 하는 객체가 데이터베이스와 연관되어 있다면, 이를 가짜 데이터베이스 역할을 하는 FakeUserRepository를 만들어 테스트 객체에 주입한다.

이를 통해, 테스트 객체는 데이터베이스에 의존하지 않으면서도 동일하게 동작을 하는 Fake 객체를 만들 수 있다.

3. Stub

  • Dummy 객체가 실제로 동작하는 것 처럼 보이게 만들어 놓은 객체이다.
  • 인터페이스 또는 기본 클래스가 최소한으로 구현된 상태
  • 테스트에서 호출된 요청에 대해 미리 준비해둔 결과를 제공한다.
public interface UserRepository {
    void save(User user);
    User findById(long id);
}
public class StubUserRepository implements UserRepository {
    // ...
    @Override
    public User findById(long id) {
        return new User(id, "Test User");
    }
}

StubUserRepository 의 findById() 메서드를 호출 할 경우, 언제나 동일한 id 값에 Test User라는 이를을 가진 인스턴스를 반환 받게 된다.

이처럼 테스트를 위해 의도한 결과만 반환되도록 하기 위한 객체가 stub 이다.

4. Spy

  • Stub의 역할을 가지면서 호출된 내용에 대해 약간의 정보를 기록한다.
  • 테스트 더블로 구현된 객체에 자기 자신이 호출 되었을 때, 확인이 필요한 부분을 기록하도록 구현
  • 실제 객체처럼 동작시킬 수도 있고, 필요한 부분에 대해서는 Stub로 만들어서 동작을 지정할 수도 있다.
public class MailingSpyService implements MailingService {
    private int sendMailCount = 0; // spy 추가
    private Collection<Mail> mails = new ArrayList<>();
		
		@Override
    public void sendMail(Mail mail) {
        sendMailCount++; // spy 추가
        mails.add(mail);
    }
		// spy 추가
    public long getSendMailCount() {
        return sendMailCount;
    }
}

MailingSpyService는 sendMail을 호출 횟수를 물어볼때, getSendMailCount 메서드를 통해 sendMailCount 를 확인 할 수 있다.

이처럼 자기 자신이 호출된 상황을 확인할 수 있는 객체가 Spy이다.

5. Mock

  • 호출에 대한 기대를 명세하고, 내용에 따라 동작하도록 프로그래밍 된 객체이다.
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    
    @Test
    void test() {
        when(userRepository.findById(anyLong())).thenReturn(new User(1, "Test User"));
        
        User actual = userService.findById(1);
        assertThat(actual.getId()).isEqualTo(1);
        assertThat(actual.getName()).isEqualTo("Test User");
    }
}

userService 인터페이스 구현체가 findById() 메서드를 호출하였을 때 어떤 결과를 반환할지 결정할 수 있다.

Mockito

  • 개발자가 동작을 직접 제어할 수 있는 가짜(Mock) 객체를 지원하는 테스트 프레임워크

Java에서는 보통 외부와의 해당 기술들을 사용하기 위해 JUnit과 Mockito 프레임워크를 함께 사용한다.

@Mock: Mock 객체를 만들어 반환해주는 어노테이션
@Spy: Stub하지 않은 메소드들은 원본 메소드 그대로 사용하는 어노테이션
@InjectMocks: @Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션

public class MembmerService{
        private MemberDao dao;

        public Member createMember(Member member){
            if(dao.getMemberCount(member) > 0){
                    ...
                    throw new Exception();
            }
            ...
        }

}

public class MemberDao{
        public int getMemberCount(Membmer member){
                
        }
}

// JUnit과 함께 사용하기 위해 테스트 클래스에 아래 annotation 추가
@ExtendWith(MockitoExtension.class)
class MemberTest {

    @Mock
		MemberDao memberDao;
		
		@InjectMocks
		MemberService memberService;
		
		@Test
		public void test(){
	    when(memberDao.getMemberCount()).thenReturn(0); //mock에 대한 기대 명세
	
	    Member member = new Member("corn", 25);
	    assertThat(memberService.createMember(member), is(member));
		}

		...

}

Reference

0개의 댓글