본문 바로가기
Baeldung번역&공부/Spring-test

Service Layer에 대한 깨지기 쉬운 테스트를 피하라(Avoid Brittle Tests for the Service Layer)

by ms727 2025. 2. 6.

원본 글: https://www.baeldung.com/testing-the-java-service-layer#templates

 

Avoid Brittle Tests for the Service Layer Baeldung

How to avoid brittle and inflexible tests for the Service Layer of a Spring web app - mock out the interactions with the database, using JUnit, Mockito and Hamcrest.

www.baeldung.com

 

 

Service 레이어를 테스트하기 위한 많은 방법들이 있습니다.

 

이 글의 목적은 모킹을 이용해서 데이터베이스와 완벽히 격리된 유닛 테스트를 작성하는 것입니다.

 

테스트에 필요한 의존성으로 Spring, Junit, Hamcrest, Mockito를 추가해줘야합니다.

 

 

1. The Layers_

 

일반적으로 Java 웹 애플리케이션은 DAL/DAO layer위에 Service layer가 존재합니다. 

 

1.1 The Service Layer

@Service
public class FooService implements IFooService{

   @Autowired
   IFooDAO dao;

   @Override
   public Long create( Foo entity ){
      return this.dao.create( entity );
   }

}

 

1.2 The DAL/DAO Layer

@Repository
public class FooDAO extends HibernateDaoSupport implements IFooDAO{

   public Long create( Foo entity ){
      Preconditions.checkNotNull( entity );

      return (Long) this.getHibernateTemplate().save( entity );
   }

}

 

전체적인 코드는 맨 밑에 링크를 첨부하겠습니다.

 

2. Motivation and Blurring the Lines of the Unit Test

Service를 단위 테스트할 때 보통 서비스 클래스를 기본 단위로 테스트를 진행합니다. 테스트에서는 하위 레이어(이 경우 DAL/DAO)를 모킹하고 상호 검증합니다. DAO layer에서도 마찬가지입니다. database(이 경우 HibernateTemplate)를 모킹하고 상호간 검증을 진행합니다.

 

이러한 방식은 깨지기 쉬운 테스트를 만들 가능성이 있습니다. layer가 추가되거나 삭제될때마다 아마도 테스트는 다시 작성되어야할겁니다. Service layer - DAO layer - persistence layer까지 이르기까지의 지속성 작업을 하나의 단위로 보면 이 문제를 해결할 수 있습니다.

이제 단위 테스트는 서비스 레이어(Service Layer)의 API를 사용하며,
HibernateTemplate 같은 실제 영속성 계층(raw persistence)은 목(mock) 처리합니다.

즉, DAO 구현이 변경되더라도 테스트를 다시 작성할 필요가 없어집니다.
왜냐하면 테스트는 서비스 계층을 통해 데이터베이스와 상호작용하는 방식을 검증하는 것이기 때문입니다.

public class FooServiceUnitTest{

   FooService instance;

   private HibernateTemplate hibernateTemplateMock;

   @Before
   public void before(){
      this.instance = new FooService();
      this.instance.dao = new FooDAO();
      this.hibernateTemplateMock = mock( HibernateTemplate.class );
      this.instance.dao.setHibernateTemplate( this.hibernateTemplateMock );
   }

   @Test
   public void whenCreateIsTriggered_thenNoException(){
      // When
      this.instance.create( new Foo( "testName" ) );
   }

   @Test( expected = NullPointerException.class )
   public void whenCreateIsTriggeredForNullEntity_thenException(){
      // When
      this.instance.create( null );
   }

   @Test
   public void whenCreateIsTriggered_thenEntityIsCreated(){
      // When
      Foo entity = new Foo( "testName" );
      this.instance.create( entity );

      // Then
      ArgumentCaptor< Foo > argument = ArgumentCaptor.forClass( Foo.class );
      verify( this.hibernateTemplateMock ).save( argument.capture() );
      assertThat( entity, is( argument.getValue() ) );
   }

}

 

원본 글에서는 다음과 같은 예제를 통해서 테스트를 진행하는데.. 잘되지 않고 좋은 방식은 아닌것 같습니다.

Service객체에 직접 필드에 접근해서 값을 변경하는거라.. 잘되지도 않고 추가로 이를 해결하는 방식은 적진 않겠습니다.

 

이 예제에서 의존성은 단 하나입니다. 객체가 생성될때 데이터베이스에까지 도달하나요?

마지막 테스트는 Mockito의 검증(verification) 문법을 사용하여, hibernate template의 save 메서드가 호출되었는지를 확인합니다.

또한, 메서드가 호출될 때 전달된 인자(argument)를 캡처하여 검사할 수도 있습니다.

이 테스트는 엔터티(entity)가 생성되었는지를 검증하는 역할을 하며,
특정 상태(state)를 직접 확인할 필요 없이, 메서드 호출 자체를 검증(interaction test)하는 방식입니다.

물론 이 방식에도 테스트가 필요하지만 그건 또다른 유형의 테스트입니다.

 

3. 결론

 

이 기술은 결국 더 집중된 테스트를 유도하게 되며, 이를 통해 테스트가 변화에 더 강하고 유연해집니다. 이제 테스트가 실패하는 유일한 이유는 테스트 중인 책임이 깨졌기 때문입니다.

 


이 글은 제 생각에 2011년도에 작성되었던 글이라 현재와는 많은 괴리감이 있는 글 같습니다.

 

여기서 중요하게 생각하는건 책임을 분리해서 테스트해야한다는걸 중요하게 여기는것 같습니다.