본문 바로가기
Programming/Languages (Java, etc)

[SPRING] 테스트 코드 - 테스트 소개

by kghworks 2023. 3. 21.

제 PC에선 잘 되는데요?

-> 테스트 코드는 확인해보셨어요?

 

 

목차

  • 테스트 코드를 작성하는 이유
  • JUnit framework
  • TDD
  • DI in Test
  • 정리
  • 참고

 

 테스트 코드가 중요한 것은 부인할 수 없는 사실입니다. 대부분의 회사들이 채용 조건 (우대사항)에 테스트 코드에 대한 작성법, TDD 방법론을 언급하니까요. 작성 여부와 상관없이 테스트 코드의 중요성은 모든 개발조직이 공감하고 있을 겁니다.

 

비어있는 test code

 

 실무에서 테스트 코드를 작성하시나요? 대형 플랫폼 기업들은 작성하는 것으로 알고 있고, 그 외 선진적인 개발리더들은 본인이 리드하는 팀에 테스트 코드를 작성하도록 할 것입니다.

 

 그러나 대부분의 SI 기업이나 시니어를 두지 않고 주니어 혼자 모든 것을 개발하는 환경에 놓인 개발팀이라면 경력이 몇 년이든 테스트코드의 "테" 조차 거들떠보지 않았을 수 있습니다. (지금까지 받아본 프로젝트의 test directory가 비어있지 않은 적은 단 한번....)

 

 이번 포스팅에서는 테스트 코드를 추천하고, 스프링에서 테스트 코드를 작성하는 방법과 TDD 방법론에 대해서 간략히 소개해보겠습니다.


테스트 코드를 작성하는 이유

- 웹을 통한 테스트 방식이 가진 문제점

 

스프링이 개발자에게 제공하는 가장 중요한 가치가 무엇이냐고 질문한다면 나는 주저하지 않고 객체지향과 테스트라고 대답할 것이다.

- 토비의 스프링 > 2장 테스트 145p

 

스프링 개발자라면 테스트를 만들고 효과적으로 개발에 적용하여 활용할 줄 알아야 하며, 미리 확신을 주는 테스트코드를 작성해 두고 분석, 변화, 개선 작업에 유연하게 대처할 수 있어야 합니다. 이것이 테스트 코드를 작성하는 이유이자 필요성입니다. 개발자는 개발을 진행하면서 동시에 테스트 코드도 개발 (작성)해야 합니다. 

 

 사실 우리는 (웹 개발자) 항상 테스트를 하면서 개발하고 있습니다. 웹 브라우저를 통해서 말이죠. 소스를 수정하면, 적당한 log, debug point를 지정하고, 서버를 구동하여 직접 애플리케이션을 작동해서 테스트를 합니다. 하지만 이 방식은 문제가 많습니다. 

 

  • 웹 화면을 통해 값을 입력, 기능 수행, 결과 확인 필요
  • 특정 부분만 수정해도 view, controller, service, database, local was,... 모든 레이어의 기능이 준비되어야 함
  • 테스트에 필요한 요소가 많기 때문에 테스트 결과 오류가 나도 체크해야 하는 포인트가 많음
  • 개발자가 만든 코드를 의도대로 동작하는지 화면을 통해 확인함 (소스가 아니라)

 

우리는 테스트를 하려는 최소한의 범위를 잡고 간단하고 빠르게 진행해보고 싶습니다.

 

단위 테스트 (Unit Test)

 테스트를 하는 최우선 목적은 개발자가 작성한 코드가 개발자 생각대로 돌아가는지 스스로가 확인하기 위함입니다. 따라서 테스트는 아래와 같이 작성하는 것이 좋습니다.

 

 가능하면 가장 작은 단위로 쪼개서 해당 테스트에만 집중할 수 있어야 합니다. "회원을 가져오는 method"에 대한 테스트를 하고 싶다면, was를 켜거나, 회원을 가져와서 보여주는 view를 준비하거나, 데이터를 웹브라우저를 통해 입력하거나 하는 과정은 테스트에 필요 없습니다. 오로지 회원을 가져오는 코드에 대한 테스트만을 진행하면 됩니다.

 

 이렇게 작은 단위의 테스트를 수행하는 것을 단위 테스트 (Unit test)라고 하며 이 단위는 일반적으로 작을수록 좋습니다. 테스트를 하기 위해 부가적인 action (디비 테이블 truncate, jsp request 등)이 필요하다면 이는 외부 리소스에 의존하는 것으로 간주하고, 단위테스트가 아니라고 봅니다. 그러나 부가적인 액션(디비 테이블 truncate, jsp request) 들을 자동화해서 하나의 단위 (unit)으로 묶어 단일 테스트를 만들었다면 이 또한 단위테스트로 볼 수 있습니다. 

 

java 코드를 작성하고 나서, 가장 간단하게 테스트 코드를 작성하는 방법은 해당 클래스의 main()를 활용하는 것입니다.

아래는 팜하니 유저를 가져오는  getUserByName() 메서드를 테스트하는 코드입니다.

 

public static void main(String args[]) throws SQLException, ClassNotFoundException {
    User hani = getUserByName("팜하니");
    System.out.println("test result : " + "팜하니".equals(hani.getName()));	
}

...
 
    
public User getUserByName(String name) throws , SQLException {

    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("SELECT * FROM TB_USER WHERE NAME = ?");
    ps.setString(1, name);

    ResultSet rs = ps.executeQuery();
    rs.next();
    User user = new User();
    user.setSeq(rs.getString("SEQ"));
    user.setName(rs.getString("NAME"));

    rs.close();
    ps.close();
    c.close();

    return user;
}

 

테스트 결과

 메서드를 작성한 뒤 테스트 대상이 되는 메서드를 main()에서 실행해 보고, 그 결과를 System.out.println을 이용해서 확인하도록 했습니다.

 

그러나 main()을 통한 테스트는 여러 문제점이 있습니다. 일단 수동으로 테스트 결과를 확인해야 합니다. test result가 성공인지 아닌지를 알기 위해 위 코드에서 가져온 User의 이름이 "팜하니" 인지 아닌지를 검사했습니다.

 

 애플리케이션 규모가 커짐에 따라 모듈이 많아지면 반복 작업이 시작됩니다. 너무 번거롭습니다. 클래스를 만들 때마다 main()을 작성하고, 테스트 코드를 작성해야 한다면 너무 귀찮아집니다. 개발보다 테스트를 작성하고, 수행하는 일이 너 부담스럽게 다가옵니다. 그러면서 자연스럽게 테스트 코드를 빼버립니다.

 


JUnit framework

 JUnit framework (이하 JUnit)는 테스트를 수행하고, 편리하게 관리하는 platform입니다. JUnit은 JVM 코드의 testing framework를 론칭할 때 기반을 제공해 줍니다.Java spring으로부터 독립적인 framework입니다. SPRING Framework 자체도 JUnit framework를 이용하여 테스트 코드를 작성하며 개발된 framework입니다.  

 

JUnit을 사용하는 방법은 아래와 같습니다. (spring boot 기준)

* 여기에선 JUnit framework의 간략한 적용 예시만 보여드립니다. 상세 코드는 github에 올려두었습니다.

 

@RunWith(SpringRunner.class) // JUnit framework의 테스트 실행방법 확장
@SpringBootTest(classes = DaoFactorySpring.class) //ApplicationContext bean config class 명시
public class UserTest {

    /**
     * Fixture : 테스트에 필요한 object, info
     */
    @Autowired
    private GoodDAO goodDAO;


    public static void main(String args[]) throws SQLException, ClassNotFoundException {
        JUnitCore.main("com.tob.part2.UserTest");
    }


    @Test
    public void existHani() throws SQLException, ClassNotFoundException {
        User hani = goodDAO.getUserByName("팜하니");
        assertThat(hani.getName(), is("팜하니"));

    }
    
    @Test
    public void existIVE() throws SQLException, ClassNotFoundException {
        GirlGroup ive = goodDAO.getGirlGroupByName("아이브");
		assertThat(ive.getGroupName(), is("아이브"));
    }
}

 

 @Test가 2개 이상일 때 JUnit은 @Test의 순서를 보장하지 않습니다. 테스트 메서드의 실행 순서가 테스트 결과에 영향을 준다면 그건 잘못 만들어진 테스트이기 때문입니다. 

 

 JUnit은 테스트를 검증할 수 있는 여러 조건들을 아주 편리하게 제공하는데 여기에선 예외 처리에 대해서만 기술해 보겠습니다. 만일 테스트 메서드를 진행하는데 특정 Exception이 발생하는 것을 테스트해봐야 한다면 아래와 같이 @Test의 expected 속성으로 이 테스트는 예외를 기대한다는 것을 명시해 줍니다.

 

@Test(expected = EmptyResultDataAccessException.class) //test 중 해당 exception 을 expected
public void existAespa() throws SQLException, ClassNotFoundException {
    goodDAO.getUsersByNameGroup("에스파");
}

위 테스트는 EmptyResultDataAccessException이 발동하지 않으면 failed test 가 됩니다. 

 


TDD

: test-driven development (테스트 주도 개발)

 

실패한 테스트를 성공시키기 위한 코드만 작성한다.

 

 테스트 코드를 먼저 작성하고 테스트 코드를 성공하게 해주는 코드를 작성하는 방식의 개발법을 테스트 주도 개발 (이하 TDD)라고 합니다. (혹은 TFD, Test First Development, 테스트 우선 개발)

 

 보통 개발을 하다 보면 일정에 얽매여 코딩하기 바쁘고 그러다 보면 테스트 코드는 자연스럽게 뒷전이 되기 마련입니다. 이에 대안으로 TDD는 애초에 테스트를 먼저 만들고 테스트를 성공하도록 만드는 코드를 작성하는 식으로 개발하게 합니다. 이때 (테스트 작성 -  테스트 코드를 성공시키는 코드 개발) 사이클의 주기를 가능한 짧게 가져가도록 권고합니다. (보통 몇십 분 수준) TDD로 개발을 하다 보면 자연스럽게 단위 테스트가 작성되어 있습니다. TDD가 권고하는 바는 아래와 같습니다.

 

  • 테스트를 작성하고 코드를 개발하는 주기는 가능한 짧게 (몇 십분 수준으로)
  • 테스트 코드를 작성한 뒤 빨리 실행이 가능해야 함

 

 에스파 그룹을 가져오는 메서드 개발 과정을 다시 보겠습니다.

 method 실행 시 존재하지 않는 데이터를 가져오면 어떻게 할지 고민한 뒤, EmptyResultDataAccessException를 던져야겠다고 생각할 수 있습니다. 이렇게 테스트코드를 작성한 뒤 EmptyResultDataAccessException를 던지는 실제 코드를 구현하는 겁니다.

 

@Test(expected = EmptyResultDataAccessException.class) //test 중 해당 exception 을 expected 중
public void existAespa() throws SQLException, ClassNotFoundException {
    goodDAO.getUsersByNameGroup("에스파");
}

 

실제 위 테스트 코드는 아래와 같은 개발 순서로 진행할 것입니다.

 

  1. 테스트 설계 : getUsersByNameGroup() 호출 시 리턴 받는 데이터가 없다면 EmptyResultDataAccessException 발동
  2. EmptyResultDataAccessException를 감지하는 test code existAespa() 작성
  3. getUsersByNameGroup() 구현 부에 EmptyResultDataAccessException 발동하는 로직 추가
  4. test code 확인

 

 3번에서 비로소 실제 코드를 구현하는 프로세스를 확인하실 수 있습니다. 이렇게 테스트 케이스를 먼저 작성한 뒤 (2번) 테스트 케이스에 걸맞은 실제 코드를 구현하는 (3번) 방법이 TDD입니다.

 


DI in Test

- Test 작성 시 Dependecy Injection 방안

 

 스프링에서 테스트 코드 작성 시 고민해야 하는 부분은 ApplicaionContext를 어떻게 사용할 것인가입니다. 실제 테스트 코드에서 사용할 객체들이 Bean이라면 ApplicationContext를 거칠 수밖에 없습니다. (bean을 IoC에서 꺼내와야 되기 때문에)

 

 결론부터 말씀드리면, 아래 순서대로 우선순위를 가지고 고려하시기 바랍니다.

 

  1. 스프링 IoC 컨테이너 없이 테스트할 수 있는 방안이 있다면 최우선 고려
  2. 스프링 설정을 통한 DI 방식을 테스트에 이용
  3. 예외적인 의존 관계의 경우 코드에서 DI를 직접 구현

 

 1번이 최우선인 이유는 테스트 코드 자체가 간결하기 때문입니다. 또한 IoC 컨테이너를 초기화하는 작업이 Test case 마다 없기 때문에 테스트 진행 속도가 굉장히 빠릅니다. 따라서 테스트를 위한 오브젝트 생성, 초기화가 단순하고, 테스트 작업 속도를 감안한다면 1번을 최우선으로 고려하시기 바랍니다.

 2번은 테스트를 위한 별도의 DI 설정 파일을 두고 진행해야 할 경우에 대안이 됩니다. 아래와 같이 테스트 실행 시 사용할 별도의 Bean 설정 파일을 두고 사용하는 것이 방법이 될 수 있습니다.

// test 를 위한 설정 class 적용
@SpringBootTest(classes = DaoFactorySpringTest.class) 
public class UserTest {

	...
}

 만일 배포 환경 (로컬-테스트-운영) 별로 Dependency Injection 정보를 달리 관리하는 경우 이 방법이 장점이 될 수 있습니다.

 3번은 예외적으로 테스트 코드에 한해서만 강제로 의존관계를 지정해야 하는 경우입니다. 이 때는 3번이 유용할 수 있으나 가장 마지막 우선순위를 두도록 하세요. 이 방법을 사용하시려면 @DirtesConext를 사용해서 각 test case (혹은 test class) 안에서 ApplicationContext의 구성을 (의존 관계 정보) 변경할 수 있음을 명시하고, 테스트 진행 중 ApplicationContext를 적절하게 초기화할 수 있도록 해야 합니다. 앞서 언급했든 각 test case들은 서로에게 영향을 주거나 테스트 결과에 영향을 주면 안 되는 것이 원칙이기 때문입니다.

 

@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)
public class UserTest {

	...
}

 


 

정리

 테스트 코드를 작성해야 하는 이유와 java code 작성 시 테스트 작성에 유용한 JUnit framework를 알아보았습니다. 아래는 테스트 코드 작성 시 유의점입니다. 

 

  • 자동화된 테스트에 빠르게 실행이 가능한 테스트여야 함
  • main()보다는 JUnit framework가 유용
  • 테스트 결과는 일관성이 있어야 하고, 테스트 실행 순서가 결과에 영향을 주면 안 됨
  • 테스트는 포괄적이어야 함
  • 코드작성과 테스트 수행 주기가 짧을수록 효과적
  • TDD는 유용하다

참고

http://www.yes24.com/Product/Goods/7516911

 

토비의 스프링 3.1 세트 - YES24

『토비의 스프링 3.1』은 스프링을 처음 접하거나 스프링을 경험했지만 스프링이 어렵게 느껴지는 개발자부터 스프링을 활용한 아키텍처를 설계하고 프레임워크를 개발하려고 하는 아키텍트에

www.yes24.com

https://junit.org/junit5/docs/current/user-guide/

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo

junit.org

https://youtu.be/QsD1hCzaGCU?list=PLpkj8RKr48wZMPKR292FOoahqxVDi6d6R 

 

댓글