← 블로그

단위 테스트 도입기

#spring

현재 팀 내에서 개발중인 프로젝트에 단위 테스트를 도입하기 위해 시도하였던 방법을 정리한다.

모든 코드는 실제 코드가 아닌 예시 코드로 작성하였다.

테스트 할 코드

@Service
@RequiredArgsConstructor
public class Service {

	private final VaultService vaultService;
	private final DataHandler dataHandler;

	@Transactional
	public void save(RequestBody requestBody) {
		
		// 비즈니스 로직...

		dataHandler.save(requestBody);
		vaultService.save(requestBody);
	}
}

기존의 테스트 코드

@SpringBootTest( classes = Application.class )
class Test {

	@MockBean
	private VaultService vaultService;

	@SpyBean
	private DataHandler dataHandler;

	@Autowired
	private Service service;

	@Test
	@Transactional
	public void saveTest() {

		// given
		RequestBody requestBody = RequestBody.builder()
				.id( "id" )
				.name( "name" )
				.build();

		// when
		service.save( requestBody );

		// then
		Entity savedData = dataHandler.findById( "id" );
		assertThat( savedData.getName() ).isEqualTo( "name" );
	}
}

위 테스트 코드의 문제점

  • SpringBootTest를 사용해서 매우 느리다.
    • 단위 테스트는 빨라야 함.
    • SpringBootTest는 Spring과 결합되므로 단위 테스트라고 볼 수 없음.
  • 실제 DB를 호출한다.
    • VaultService의 경우 Mock을 사용하였지만 DataHandler의 경우 실제 값이 저장되었는지를 확인해야 하므로 Spy 객체(실제 객체)를 사용하였다.
    • 또한 테스트 시 H2를 사용하지 않고 기존에 사용하던 DB 서버를 연결해서 테스트하였다.
      • 테스트 코드에서 사용한 PK와 같은 PK의 데이터가 이미 존재한다면 실패하게 됨,
      • 또한 DB에 문제가 생기면 바로 실패하게 됨.
      • 비결정론적 테스트
    • 만약 H2 DB를 사용해서 테스트 한다고 해도, 매번 DDL, 사전 준비 쿼리를 실행시켜야 하는 번거로움, 추가 시간이 필요하다.

리팩토링

우선 SpringBootTest 어노테이션을 제거하자

이 때, DB와 Vault에 대한 의존성(DataHandler, VaultService)를 Stub객체로 만들면 좋겠다고 생각했다.

따라서 DataHandler와 VaultService을 인터페이스로 추상화 하고, 실제 객체와 Mock 객체를 분리할 수 있도록 하였다. (의존성 역전)

public interface DataHandler {
	void save(RequestBody requestBody);
~~}~~
public interface VaultService {
	void save(RequestBody requestBody);
}

기존의 의존성 객체들은 모두 Impl이라는 이름을 붙여줬다.

@Component
@RequiredArgsConstructor
public class DataHandlerImpl implements DataHandler {
	
	private final Repository repository;

  @Override
	public void save(RequestBody requestBody) {

		// DTO -> Entity 변환 로직...

		repository.save(entity);
	}
~~}~~
@Component
@RequiredArgsConstructor
public class VaultServiceImpl implements VaultService {

	private final VaultTemplateFactory vaultTemplateFactory;

	@Override
	public void save(RequestBody requestBody) {
		
		vaultTemplateFactory.create().save(requestBody);
	}
}

수정한 테스트 코드

// @SpringBootTest
class Test {

	private VaultService vaultService;
	**private DataHandler dataHandler;
	private Service service;

	public Test() {
		this.dataHandler = new FakeDataHandler();
		this.vaultService = new SpyVaultService();
		this.service = new Service(dataHandler, vaultService);
	}

	@Test
	public void saveTest() {

		// given
		RequestBody requestBody = RequestBody.builder()
				.id( "id" )
				.name( "name" )
				.build();

		// when
		service.save( requestBody );

		// then
		Entity savedData = dataHandler.findById( "id" );
		assertThat( savedData.getName() ).isEqualTo( "name" );
		assertThat( vaultService.saveCount ).isEqualTo( 1 );
	}
}

FakeDataHandler

public class FakeDataHandler {

	private Map<String, Entity> entityRepository = new HashMap<>();
	
	@Override
	public void save(RequestBody requestBody) {
		
		// DTO -> Entity 변환
		Map.put(requestBody.getId(), entity);
	}
	
	public Entity findById(String id) {
		
		return Map.get(id);
	**}
}

Map으로 인메모리 데이터베이스를 구현

SpyVaultService

public class SpyVaultService{

	public int saveCount;
	
	@Override
	public void save(RequestBody requestBody) {

		this.saveCount++;		
	}
}

Vault로 요청이 보내졌는지만 체크하도록 구현

위 테스트용 객체 클래스의 이름(Spy, Fake 등)은 Gerald Meszaros의 5가지 Test double 종류를 따랐다.

총 5 종류의 Test double은 다음과 같다.

  • Dummy

    객체는 전달되지만 실제로는 사용되지 않음. 일반적으로 매개변수 목록을 채우는 데 사용됨.

  • Fake

    개체에는 실제로 작동하는 구현이 있지만 일반적으로 생산에 적합하지 않게 만드는 몇 가지 지름길을 사용함(인메모리 DB가 좋은 예).

  • Stub

    테스트 중에 이루어진 호출에 대해 미리 준비된 답변을 제공하며 일반적으로 테스트를 위해 프로그래밍된 것 이외의 것에는 전혀 응답하지 않음.

  • Spy

    호출된 방식에 따라 일부 정보를 기록하는 스텁. 이에 대한 한 가지 형태는 전송된 메시지 수를 기록하는 이메일 서비스일 수 있다.

  • Mock

    수신할 것으로 예상되는 호출의 사양을 형성하는 기대로 미리 프로그래밍된 객체입니다.

출처 - 마틴 파울러 : MocksArentStubs

하지만 보통은 테스트 더블의 상태를 확인해서 검증할 때에는 Stub, 메서드를 호출했는지 를 검증할 때에는 Mock 이 두 가지를 주로 사용하는 것 같다.

이렇게 해서 Spring이나 DB와 같은 여러 외부 종속성에 영향을 받지 않는 독립적인 단위 테스트를 수행할 수 있게 되었다.