Mock Framework

2019-02-28

Mock Framework

목(Mock)이 무엇이고 왜 필요하죠?

우리가 Junit 으로 테스트를 할때에는 보통 어떤 시스템에 입력을 주었을 때 기대하는 출력이 나오는지를 주로 검증하게 됩니다. 단위 테스트에서는 보통 입력 값을 테스트 대상 오브젝트의 메소드의 파라미터로 전달하고 메소드의 리턴 값을 출력 값을 보고 검증합니다

다음과 같이 말이죠!

public class TestEx {

	//평범하게 파라미터로 들어온 값을 반환하는 함수입니다.
	public int printInteger(int i) {
		return i;
	}
	
	@Test
	public void IntegerTest() {
		// 5가 출력이 되었는지 단위테스트를 해준다.
		assertThat(printInteger(5), is(5));
	}
}

하지만 이렇게 단순히 리턴 값 출력을 하는것이 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트(써드파티 라이브러리 등 내가 만들지 않고 다른사람이 만든 API)가 그 행위가 제대로 이루어 졌는지 검증하고싶다면 어떻게 할까요? 아래와 같이 검증합니다!

img

이런 경우에는 테스트 대상의 간접적인 출력 결과를 검증해야하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검증할 수 있도록 특별히 설계된 목 오브젝트(Mock Object) 라는 것이 있습니다.

목 오브젝트는 테스트 오브젝트가 정상적으로 실행되도록 도와주면서 테스트 오브젝트와 자신의 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는데 활용할 수 있게 해줍니다.

데이터베이스, 웹서버, 웹애플리케이션서버 ,FTP 서버 등 환경 구축을 위한 작업시간이 필요한 경우에 사용합니다. 특정 모듈을 갖고 있지 않아서 테스트 환경을 구축하지 못하거나 다른 사람과 협의나 정책이 필요한 경우에 사용됩니다.

예를들어 보겠습니다.

다음은 해당 회원을 삭제하는 Test 코드입니다.

@Test
public void deleteTest() {
    memberDao.deleteMember(1);
}

해당 회원번호를 파라미터로 받아서 회원을 삭제하는 메소드인데요, 어떻게 이 회원이 삭제 됐는지 확인을 할 수 있을까요?

해당 DB에 들어가서 회원이 삭제 됐는지 확인하면 됩니다. 하지만 DB를 사용하지 않는 기능들이라면요? 예를 들어 메일을 전송하는 서비스를 작성했다고 생각해봅시다. 메일이 전송됐는지 확인하는 어떻게 확인을 해야할까요?

그래서 등장한게 Mockito 프레임워크

Mockito라는 프레임워크는 사용하기도 편리하고, 코드도 직관적이라 많은 인기를 끌고 있는데요, Mockito와 같은 목 프레임워크의 특징은 목 클래스를 일일이 준비해둘 필요가 없다는 점 입니다. 간단한 메소드 호출만으로 다이내믹하게 특정 인터페이스를 구현한 테스트용 목 오브젝트를 만들 수 있습니다.

준비하기

  • Spring version : 4.3.2
  • JDK : 1.8
  • Junit4
  • 기본패키지 : com.mock.tutorial

의존 라이브러리

테스트를 하기 위한 Junit을 추가해줍시다.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

시작하기

먼저 Mockito 프레임워크를 사용하기 위해서 라이브러리를 추가해주도록 하겠습니다.

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.5</version>
</dependency>

hamcrest 라이브러리도 추가해주세요. 이 라이브러리는 다른언어에도 존재하는데요, Matcher를 좀 더 문장스럽게 꾸며주는 역할을 해줍니다.

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

이 예제는 회원가입을 한 사람에게 회원가입 축하 메일을 보내주는 코드를 작성하신다고 생각하시면 됩니다.

프로젝트에서 com.mock.tutorial.test , com.mock.tutorial.service , com.mock.tutorial.dao , com.mock.tutorial.dto 패키지를 생성해주세요.

test 패키지 밑에 MailsenderTest 클래스를 하나 생성해주세요

MailsenderTest.class

package com.mock.tutorial.test;

public class MailSenderTest {

}

MemberDto.class

package com.mock.tutorial.dto;

public class MemberDto {
	
	int no;
	String id;
	String pw;
	
	public MemberDto(int no,String id, String pw) {
		this.no = no;
		this.id = id;
		this.pw = pw;
	}
	
	public MemberDto(String id, String pw) {
		this.id = id;
		this.pw = pw;
	}
	
	public int getNo() {
		return no;
	}
	public void setNo(int no) {
		this.no = no;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getPw() {
		return pw;
	}
	public void setPw(String pw) {
		this.pw = pw;
	}
	
}

MemberDao.class

package com.mock.tutorial.dao;

import com.mock.tutorial.dto.MemberDto;

public interface MemberDao {	
	int insertMember(MemberDto member);
}

service 패키지 밑에 MailSender 인터페이스를 하나 생성하고 다음과 같이 적어주세요.

MailSender.class

package com.mock.tutorial.service;

public interface MailSender {
	void sendMail(String user_id);
}

같은 패키지안에 MemberService 인터페이스를 추가하고 다음 메소드를 선언해주세요.

public interface MemberService {
	void insertMember(MemberDto member);
}

그런 다음 오늘의 중요한 클래스인 MemberServiceImpl 클래스를 추가해주세요

package com.mock.tutorial.service;

import com.mock.tutorial.dao.MemberDao;
import com.mock.tutorial.dto.MemberDto;

public class MemberServiceImpl implements MemberService{

	MailSender mailSender;
	
	MemberDao memberDao;
	
	public void setMailSender(MailSender mailSender) {
		this.mailSender = mailSender;
	}
	
	public void setMemberDao(MemberDao memberDao) {
		this.memberDao = memberDao;
	}
	
	
	@Override
	public void insertMember(MemberDto member) {
		//dao를 통해 DB에 해당 member Insert
		memberDao.insertMember(member);
		//가입 축하 메일 보내기
		mailSender.sendMail(member.getId());
	}

}

저희는 직접적인 메일을 보내주는 서비스를 구현하진 않을거예요.

지금까지의 파일들은 바로 여기 에 있습니다.

테스트를 하기전에 MemberServiceImpl 클래스의 insertMember 함수를 보겠습니다.

public void insertMember(MemberDto member) {
    //dao를 통해 DB에 해당 member Insert
    memberDao.insertMember(member);
    //가입 축하 메일 보내기
    mailSender.sendMail(member.getId());
}

memberDao.insertMember(member) 이 코드는 Dao에게 해당 member를 데이터베이스에 추가해달라는 코드입니다.

이 코드가 정상적으로 수행 된걸 어떻게 확인 할 수 있을까요? 데이터베이스에 접근해서 해당 데이터를 뽑아와서 비교를 해보면 될 것입니다.

하지만 가입한 유저에게 가입축하 메일을 보내는 mailSender.sendMail(member.getId()) 코드는 return 값은 void 인데에다가 memberDao처럼 데이터베이스에서 해당 데이터를 확인할 수도 없고 정상적으로 메일을 보내는 함수가 호출됐는지 확인을 할 수 없는 노릇입니다. 거기다가 그저 코드를 테스트 하는것이니 테스트를 돌릴때 실제로 메일을 발송하면 안됩니다.

이 문제의 해결방법은 메일을 발송해주는 함수가 호출이 되었는지를 확인해주기만 하면 됩니다.

이제 MailSender를 테스트하는 Test 코드를 작성해보도록 하겠습니다.

MailsenderTest.class

package com.mock.tutorial.test;

import static org.mockito.Mockito.mock;
import org.junit.Test;
import com.mock.tutorial.service.MailSender;

public class MailSenderTest {
	@Test
	public void mockMailSenderTest() {
		//목 만들어주기
		MailSender mockMailSender = mock(MailSender.class);
		
	}
}

테스트용 함수인 mockMailSenderTest를 만들어서 그 안에 Mockito 프레임워크를 만들어서 MailSender만의 mock 오브젝트를 만들었습니다.

mock() 함수는 파라미터로 클래스나 인터페이스를 받습니다. 하지만 주로 인터페이스를 자주 사용하곤 합니다.

mock() 파라미터로 인터페이스를 자주 사용되는 이유가 뭔가요?

TDD(Test Driven Development) 테스트 지향적 개발에서는 클래스를 구현하기전에 먼저 인터페이스로 테스트를 합니다. 그렇기 때문에 인터페이스를 많이 쓰곤 합니다.

이제 메일을 전송해보는 테스트를 작성해보겠습니다.

MailsenderTest.class

package com.mock.tutorial.test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.Test;

import com.mock.tutorial.dao.MemberDao;
import com.mock.tutorial.dto.MemberDto;
import com.mock.tutorial.service.MailSender;
import com.mock.tutorial.service.MemberServiceImpl;

public class MailSenderTest {
	@Test
	public void mockMailSenderTest() {

		MemberServiceImpl memberServiceImpl = new MemberServiceImpl();
		//MemberDao 목 생성 
		MemberDao mockMemberDao = mock(MemberDao.class);
		//MailSender 목 생성
		MailSender mockMailSender = mock(MailSender.class);
		
		memberServiceImpl.setMailSender(mockMailSender);
		
		memberServiceImpl.setMemberDao(mockMemberDao);
		
		MemberDto member = new MemberDto("foo", "1234");
		
		//foo라는 유저 회원가입
		memberServiceImpl.insertMember(member);
		
		// sendMail() 함수가 1번 호출되었는지 확인
		verify(mockMailSender, times(1)).sendMail("foo");
	}
	
}

테스트할 MemberService를 구현한 클래스인 MemberServiceImpl을 선언을 해주었습니다.

MemberServiceImpl은 두개의 클래스를 초기화를 해줘야 하는데요, MemberDaoMailSender 입니다. 하지만 우리는 두개의 클래스를 모두 구현을 하지 않았고, 구현했더라도 그 기능을 실제로 실행할 필요는 없습니다. 그래서 두개 클래스 모두 Mock 오브젝트로 만들어 줍시다. 그저 로직이 알맞게 돌아가고 있는지 테스트를 하기 위해서입니다. 이제 MemberServiceImpl 가 의존하고 있는 두개의 클래스는 초기화가 완료되었습니다.

이제 회원가입할 유저를 MemberServiceImpl의 insertMember를 호출해서 메일까지 전송을 해보는 코드는 memberServiceImpl.insertMember(member) 입니다.

이제 MailSender 클래스의 sendMail() 함수가 유저 “foo” 에게 메일을 보내는 게 정상적으로 실행됐는지 확인하는 코드입니다.

verify(mockMailSender, times(1)).sendMail("foo"); 는 sendMail(“foo”) 호출이 1번 되었는지 확인해주는 코드입니다.

## 스텁이란?

지금의 테스트코드는 함수가 몇 번 호출됐는지만 알 수 있고, 어떤 값을 리턴해야하는지 정해주지 않았습니다. 그런데 서비스단에서 MemberDao객체의 리턴 값에 의해 분기되는 코드를 작성했다면 결과 값을 리턴해주어야 합니다.

하지만 MemberDao는 테스트할 때 Mock 객체로 변경되어서 기능이 없습니다.

이럴때 스텁을 사용합니다. 즉 고정된 결과값을 돌려주는 것이죠.

MemberServiceImpl 클래스에 있는 insertMember 함수에 스텁을 추가 해보도록 하겠습니다.

MemberServiceImpl 클래스로 가서 insertMember() 함수를 다음과 같이 변경해보겠습니다.

MemberServiceImp.class

	@Override
	public void insertMember(MemberDto member) {
		//dao를 통해 DB에 해당 member Insert
		int result = memberDao.insertMember(member);
		
		if(result == 0) {
			System.out.println("회원 가입 실패 !!");
		} else {
			System.out.println("회원 가입 성공 !!");
			//가입 축하 메일 보내기
			mailSender.sendMail(member.getId());
		}
			
	}

SQL에서 Insert가 성공하면 1을 리턴하고 실패하면 0을 리턴하게 됩니다. 그 결과 값에 따라 분기를 해주는 코드를 작성했습니다.

그럼 이제 테스트코드는 어떻게 작성해야 될까요? 이럴땐 어떤 결과 값이 나오도록 스텁을 이용하면 됩니다.

MailsenderTest.class

public class MailSenderTest {

	@Test
	public void mockMailSenderTest() {

		MemberServiceImpl memberServiceImpl = new MemberServiceImpl();
		//MemberDao 목 생성 
		MemberDao mockMemberDao = mock(MemberDao.class);
		//MailSender 목 생성
		MailSender mockMailSender = mock(MailSender.class);
		
		memberServiceImpl.setMailSender(mockMailSender);
		
		memberServiceImpl.setMemberDao(mockMemberDao);
	
		MemberDto member = new MemberDto("foo", "1234");
		//스텁 생성 
		//Dao의 insertMember가 호출되면 0을 리턴하라는 뜻
		when(mockMemberDao.insertMember(member)).thenReturn(1);
		
		//foo라는 유저 회원가입
		memberServiceImpl.insertMember(member);
		
		// sendMail() 함수가 1번 호출되었는지 확인
		verify(mockMailSender, times(1)).sendMail("foo");
	}
	
}

when(mockMemberDao.insertMember(member)).thenReturn(1); 이 소스는 insertMember가 호출되면 1을 리턴하라는 뜻입니다.

테스트코드를 실행하면 회원 가입 성공 !! 이 뜨는걸 확인하실 수 있습니다.

반대로 0을 주게되면 테스트코드가 실패하게 됩니다.

이렇게 실제로 DB연결 구현은 안했지만, 추후에 다른팀원이 구현할걸 예상에서 미리 작성하여 협업에 많은 도움을 줄 수 있습니다.

완성된 코드는 여기 에 있습니다.

마치며

TDD(Test Drvice Development)인 테스트 지향적 개발은 정말 중요합니다. 잘 만들어진 테스트 코드는 몇초만에 버그를 발견하여 시간적 경제적 비용이 적게들게하고 프로그램 개발단축시간을 많이 줄일 수 있습니다.

테스트 코드를 많이 사용하도록 노력해봅시다!