내가 작성한 코드를 확신하기 위해서. 두 발 뻗고 자러가기 위해.
"테스트 없는 스프링은 의미가 없다"
관심에 따라 분리하라. 쪼갤 수 있을만큼 쪼개라.
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
을 활용하자. 참고로 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() 두 개의 메소드를 추가함.
'@Test
&& public void
&& no parameters'인 method를 모두 찾는다.
테스트를 대상이 되는 클래스의 Object를 하나 만든다.
@Before
메소드가 있다면 실행
@Test
메소드를 하나 실행하고 결과를 저장
@After
메소드가 있다면 실행
모든 @Test
메소드에 대해 2 ~ 5 Steps 반복
테스트 결과를 취합해 리턴
매번 독립적인 객체를 만드므로, 각 테스트가 독립적인 환경임을 보장한다.
@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 할 수 있지만 일반적으로는 인터페이스 권장 (느슨한 연결을 권장함)
ex) 테스트에는 테스트용 DB를 사용하고 싶을 때 DataSource를 임의로 갈아끼워야 함
테스트 코드에 의한 DI : @Before
메소드에서 setter로 DI 직접 삽입 + @DirtiesContext
를 클래스나 메소드에 걸어서 해당 애플리케이션 컨텍스트를 클래스/메소드 간 공유하지 않도록 명시 🥈
별도의 DI 설정 : test-applicationContext.xml
를 만들어서, 이 파일에서 Test DB 세팅을 하고, 테스트 클래스의 @ContextConfiguration
에 이 파일을 명시 🥉
(스프링) 컨테이너 없이 DI : Context 공유 다 빼고, 1과 같음. 🥇
선테스트후코딩 굿
🤔 사견 : 2010년대 초반에 TDD가 온갖 곳에서 언급되었기 때문에 이렇게 유명해진걸까?
자신이 만들지 않은 프레임워크, 제공받은 라이브러리 등에 대한 테스트를 학습 테스트라 한다.
다양한 조건에 따른 기능을 손쉽게 & 자동화하여 확인할 수 있다.
개발 중 학습 테스트 코드를 참고할 수 있다.
프레임워크/라이브러리 업그레이드 시 호환성 검증용으로 사용
테스트 작성에 대한 훈련을 도와줌
내가 만든 테스트가 통과함으로써 공부의 과정이 즐거워진다!
코드에 오류가 있을 때는, 그 오류를 가장 잘 드러내는 테스트를 작성해야 하는데, 이를 버그 테스트라고 한다. 버그 테스트는 처음에 실패하도록 만들어야 한다.
테스트의 완성도가 업그레이드 된다 : 기존에는 불충분했던 부분을 보완하는 효과.
버그의 내용을 명확하게 분석하게 해준다 : 테스트로 만들기 위해 면밀히 관찰해야 한다. 이 과정에서 새로운 버그를 찾을 수도 있다.
나올 수 있는 결과를 모두 체크하라. (ex. True/False/NullException : 3 Tests)
경계값 근처를 유의깊게 확인하라. (0, 1, -1, MAX_INTEGER, OVERFLOW)
기술적인(technical) 문제를 해결하는데 도움이 된다 : 그냥 해결할 때보다 문제가 명쾌해지고, 외부의 도움을 요청할 때 편리하다.
자동화된 & 빨리 실행할 수 있는 & 포괄적인 테스트가 좋다
main()보다 JUnit 권장
테스트 결과는 반복해도 일관되어야 한다. 즉, 외부 환경, 테스트 실행 순서의 영향을 받지 않아야 한다.
'코드 작성 ~ 테스트 수행'의 간격이 짧을수록 좋다
테스트하기 쉬운 코드 == 좋은 코드
TDD 짱짱맨
테스트 코드에 대한 리팩토링도 잊지 말 것
@Before, @After, @ContextConfiguration, @RunWith, @Autowired
학습 테스트 하자
버그 바로 해결하지 말고 버그 테스트 먼저 만들자