토비의 스프링 3.1 - 2장_테스트

Roeniss Moon·2020년 6월 22일
0

토비의 스프링 3.1

목록 보기
3/6
post-thumbnail

왜 테스트를 해야하는가

내가 작성한 코드를 확신하기 위해서. 두 발 뻗고 자러가기 위해.

"테스트 없는 스프링은 의미가 없다"

테스트를 만드는 기준 - 단위 테스트 (unit test)

'단위'를 어떻게 정하는가

관심에 따라 분리하라. 쪼갤 수 있을만큼 쪼개라.

  • DB를 사용하면 단위 테스트가 아니다 (X)

  • DB의 상태를 테스트 코드가 관장하고 있다면 단위 테스트다 (O)

    • == "테스트를 위해 DB를 특정 상태로 통제할 수 있다"

    • 즉, "통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니다"

테스트를 작게 쪼갤 때의 장점

개발자 스스로 빨리 확인받을 수 있다.

주의할 점

  • 항상 일관성 있는 결과가 보장되어야 한다.

  • 포괄적인 테스트를 진행한다.

    • 모든 엣지/코너 케이스를 고려한다 - "항상 네거티브 테스트를 먼저 만들어라" by 로드 존슨(스프링 창시자)

    • 예외 케이스를 놓치지 말라는 말이다.

테스트를 어떻게 할 것인가

자동화의 필요성

현재 기존 테스트 코드는 다음과 같다.

package springbook.user.dao;
// import 생략
public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        System.out.println("UserDao 테스트 시작");

        ApplicationContext context = new GenericXmlApplicationContext("/applicationContext.xml");
        UserDao dao = context.getBean("userDao", UserDao.class);

        User testUser = new User("roeniss", "꿔바로우 먹고싶다", "1q2w3e4r!"); // id, name, password
        dao.add(testUser);
        System.out.println("유저 추가 성공 : " + testUserId);

        User testUser2 = dao.get(testUserId);
        System.out.println("유저 탐색 성공 : " + testUser2);

        System.out.println("UserDao 테스트 종료");
    }
}

이 코드는 몇 가지 문제점이 있다.

  • 콘솔을 읽고 확인해야 눈으로 함

  • 여러 패키지에 여러 main에 걸쳐 테스트들이 존재한다면 각각을 일일이 실행시켜 주어야 한다.

솔루션 : JUnit

JUnit

JUnit을 활용하자. 참고로 xUnit '프레임워크'를 만든 사람은 켄트 벡이다. Java 포팅(JUnit)도 주도한 듯.

🤔 사견 : 책에선 'com.springsource.org.junit-4.7.0.jar'을 요구했는데, 나는 인텔리제이에 내장된 4.12, hamcrest-core-1.3을 사용함. (아... 쓰면서 알았는데 4.1.2.가 아니고 4.12구나..)

macOS 기준, IntelliJ에서 Ctrl+Space를 두번 입력해서 static import에 대한 basic completion을 사용할 수 있다.
(https://blog.jetbrains.com/idea/2012/08/complete-static-methods-and-fields-with-the-new-intellij-idea-12-eap-build-12229/)

IDE 도움 없이 실행하기 위해선,

public class UserDaoTest {
    public static void main(String[] args){
        // UserDaoTest 클래스에서 @Test 어노테이션을 보고 테스트 실행
        JUnitCore.main("springbook.user.dao.UserDaoTest")
    }
    
    @Test
    public void addAndGet(){
        // dao.add(), dao.get()을 테스트
    }
}

(IDE 도움을 받거나, 빌드 과정에 끼워넣는 게 일반적)

이 타이밍에 UserDao에 deleteAll(), getCount() 두 개의 메소드를 추가함.

JUnit

특징

  • 각 테스트 메소드의 실행 순서를 보장하지 않는다.

JUnit가 테스트를 수행하는 순서

  1. '@Test && public void && no parameters'인 method를 모두 찾는다.

  2. 테스트를 대상이 되는 클래스의 Object를 하나 만든다.

  3. @Before 메소드가 있다면 실행

  4. @Test 메소드를 하나 실행하고 결과를 저장

  5. @After 메소드가 있다면 실행

  6. 모든 @Test 메소드에 대해 2 ~ 5 Steps 반복

  7. 테스트 결과를 취합해 리턴

매번 독립적인 객체를 만드므로, 각 테스트가 독립적인 환경임을 보장한다.

@Before/@After method에서 생성한 변수를 test method로 보낼 수 없어서 인스턴스 변수들을 활용함. 혹은 test methods에서 직접 특정 메소드를 호출하는 방법도 있음.

컨텍스트와 빈은 재사용 하자

  • ApplicationContext는 초기화되고 나면 내부의 상태가 바뀌는 일이 '거의' 없다.

  • Bean은 싱글톤으로 만들었기 때문에 stateless하다.

이런 상황이므로, Spring의 Test Context Framework를 사용해보기로 한다. 이를 통해 "테스트 클래스 내에서 컨텍스트를 공유할 수 있다." 여러 테스트 클래스가 같은 설정 파일을 이용한다면 이 또한 하나의 컨텍스트를 공유한다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;
    // ...
}
  • @RunWith(SpringJUnit4ClassRunner.class) : JUnit용 테스트 컨텍스트 프레임워크(를) 확장(사용하는) 클래스를 지정한다 - "테스트에서 사용할 Application Context를 만들고 관리한다" - JUnit이 테스트를 진행하는 중에 이를 사용할 수 있게 됨

  • @ContextConfiguration(locations = "/applicationContext.xml") : 어떤 컨텍스트 설정 파일 위치 명시

  • @Autowired : 변수 타입과 일치하는, 해당 컨텍스트 내의 빈을 찾는다. 생성자, setter method 없이 주입 가능하다. 같은 타입의 빈이 두 개 이상이면 잘 안될 수 있음.

    • 컨텍스트가 초기화될 때 자기 자신도 빈으로 등록하기 때문에 위 코드가 가능함.

    • 클래스와 인터페이스로 autowire 할 수 있지만 일반적으로는 인터페이스 권장 (느슨한 연결을 권장함)

테스트에 DI 사용하기

ex) 테스트에는 테스트용 DB를 사용하고 싶을 때 DataSource를 임의로 갈아끼워야 함

  1. 테스트 코드에 의한 DI : @Before 메소드에서 setter로 DI 직접 삽입 + @DirtiesContext를 클래스나 메소드에 걸어서 해당 애플리케이션 컨텍스트를 클래스/메소드 간 공유하지 않도록 명시 🥈

  2. 별도의 DI 설정 : test-applicationContext.xml를 만들어서, 이 파일에서 Test DB 세팅을 하고, 테스트 클래스의 @ContextConfiguration에 이 파일을 명시 🥉

  3. (스프링) 컨테이너 없이 DI : Context 공유 다 빼고, 1과 같음. 🥇

  • 스프링은 비침투적(non-invasive) 기술이다 : 코드가 스프링을 사용하더라도 스프링에 종속되지 않는다(= 언제든지 제거할 수 있다).

TDD

선테스트후코딩 굿

🤔 사견 : 2010년대 초반에 TDD가 온갖 곳에서 언급되었기 때문에 이렇게 유명해진걸까?

학습 테스트 (learning test)

자신이 만들지 않은 프레임워크, 제공받은 라이브러리 등에 대한 테스트를 학습 테스트라 한다.

목적과 장점

  • 다양한 조건에 따른 기능을 손쉽게 & 자동화하여 확인할 수 있다.

    • 자동화 : 눈으로 콘솔을 읽을 필요가 없음. ("Success or Fail")
  • 개발 중 학습 테스트 코드를 참고할 수 있다.

  • 프레임워크/라이브러리 업그레이드 시 호환성 검증용으로 사용

  • 테스트 작성에 대한 훈련을 도와줌

  • 내가 만든 테스트가 통과함으로써 공부의 과정이 즐거워진다!

버그 테스트 (bug test)

코드에 오류가 있을 때는, 그 오류를 가장 잘 드러내는 테스트를 작성해야 하는데, 이를 버그 테스트라고 한다. 버그 테스트는 처음에 실패하도록 만들어야 한다.

목적과 장점

  • 테스트의 완성도가 업그레이드 된다 : 기존에는 불충분했던 부분을 보완하는 효과.

  • 버그의 내용을 명확하게 분석하게 해준다 : 테스트로 만들기 위해 면밀히 관찰해야 한다. 이 과정에서 새로운 버그를 찾을 수도 있다.

    • 나올 수 있는 결과를 모두 체크하라. (ex. True/False/NullException : 3 Tests)

    • 경계값 근처를 유의깊게 확인하라. (0, 1, -1, MAX_INTEGER, OVERFLOW)

  • 기술적인(technical) 문제를 해결하는데 도움이 된다 : 그냥 해결할 때보다 문제가 명쾌해지고, 외부의 도움을 요청할 때 편리하다.

정리

  • 자동화된 & 빨리 실행할 수 있는 & 포괄적인 테스트가 좋다

  • main()보다 JUnit 권장

  • 테스트 결과는 반복해도 일관되어야 한다. 즉, 외부 환경, 테스트 실행 순서의 영향을 받지 않아야 한다.

  • '코드 작성 ~ 테스트 수행'의 간격이 짧을수록 좋다

  • 테스트하기 쉬운 코드 == 좋은 코드

  • TDD 짱짱맨

  • 테스트 코드에 대한 리팩토링도 잊지 말 것

  • @Before, @After, @ContextConfiguration, @RunWith, @Autowired

  • 학습 테스트 하자

  • 버그 바로 해결하지 말고 버그 테스트 먼저 만들자

profile
기능이 아니라 버그예요

0개의 댓글