Today I Learned_230505
토비의 스프링 3.1
2장. 테스트
스링이 개발자에게 제공하는 가장 중요한 가치
→ 객체지향과 테스트
DI는 스프링의 핵심으로, 오브젝트의 설계, 생성, 관계, 사용에 관한 기술
스프링은 IoC/DI를 이용해 객체지향 프로그래밍 언어의 근본과 가치를 개발자가 손쉽게 적용하고 사용할 수 있게 도와주는 기술
또한, 복잡한 엔터프라이즈 애플리케이션을 효과적으로 개발하기 위한 기술
→ 이걸 개발하기 위해서는 객체지향 필요
→ 테스트도 필요
테스트 기술 : 만들어진 코드를 확신할 수 있게 하고, 변화에 유연하게 대처할 수 있다.
테스트의 유용성
테스트는 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서 만든 코드를 확신할 수 있게 해주는 작업
테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다.
웹을 통한 테스트의 문제점 → 모든 레이어의 기능을 다 만들어야지 테스트가 가능
뷰, 컨트롤러, 서비스 클래스를 다 만들어야 테스트가 가능하다.
테스트 하는 중에 에러가 나거나 테스트가 실패했다면 문제가 생긴 곳을 찾기 어렵다.
단위 테스트
- 작은 단위의 코드에 대해 테스트를 수행한 것
- 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다.
- 단위의 정의는 명확하지 않지만, 일반적으로 단위는 작을수록 좋다.
- 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 한다.
- 테스트하는데 외부의 DB 상태에 영향을 받는 등..
- 멱등성이 보장되지 않는 상황..
- 개발자 테스트 또는 프로그래머 테스트라고도 한다.
단위 테스트가 필요한 이유
- 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위함
- 단위별로 충분한 검증을 한 뒤에 긴 테스트를 한다면 오류가 덜 할 것
자동수행 테스트 코드
- 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
- 테스트를 자주 수행하는데 부담이 들면 안 된다.
- 자동수행 테스트 코드의 장점은 자주 반복할 수 있다는 것이다.
기존 테스트와 그 문제점
public class UserDaoTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("user");
user.setName("강희정");
user.setPassword("123");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
- main 메소드 사용
- 테스트 대상인 UserDao 직접 호출
- 수동 확인 작업의 번거로움
- 결과를 사람의 눈으로 확인해서 기록하고 정리해야 한다.
테스트 검증의 자동화
모든 테스트는 성공과 실패의 두 가지 결과를 가질 수 있다.
테스트의 실패는 두 가지로 나뉠 수 있음
- 테스트 에러 : 테스트가 진행되는 동안 에러가 발생해서 실패하는 경우
- 테스트 실패 : 에러는 나지 않았지만 결과가 기대한 것과 다르게 나오는 경우
테스트 에러는 즉각적인 확인이 가능하지만, 테스트 실패는 눈으로 직접 확인해야 함 → 자동화 필요
테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해 주는 것!
JUnit을 사용한 테스트의 효율적인 수행과 결과 관리
JUnit : 프로그래머를 위한 자바 테스팅 프레임워크
자바로 단위 테스트를 만들 때 유용하게 쓸 수 있다.
- 일정한 패턴을 가진 테스트를 만들 수 있다.
- 많은 테스트를 간단히 실행시킬 수 있다.
- 테스트 결과를 종합해서 볼 수 있다.
- 테스트가 실패한 곳을 빠르게 찾을 수 있다.
- 위 기능들을 지원하는 테스트 작성 방법이 별도로 필요.
프레임워크의 기본 동작원리는 제어의 역전!
- 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어
- 클래스의 오브젝트를 생성하고 실행하는 일은 프레임워크에 의해 진행
- 프레임워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드도 필요 없다.
JUnit 프레임워크가 요구하는 조건
- 메소드가 public으로 선언되어야 함
- 메소드에 @Test 어노테이션을 붙여 줘야 함
- retrun값이 void형이고 파라미터가 없어야 한다.
JUnit을 사용하여 테스트코드 수정
package com.example.demo.dao;
import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import java.sql.SQLException;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class UserDaoTest {
@Test // JUnit에게 테스트용 메소드임을 알려줌
// 반드시 public으로 선언되어야 한다.
public void addAndGet() throws ClassNotFoundException, SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("user");
user.setName("강희정");
user.setPassword("123");
dao.add(user);
User user2 = dao.get(user.getId());
// org.junit.Assert.assertThat은 deprecated되었지만 우선은 예제에 따르기로 한다.
assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword(), is(user.getPassword()));
}
// JUnit 시작부분
public static void main(String[] args) throws ClassNotFoundException, SQLException {
JUnitCore.main("com.example.demo.dao.UserDaoTest");
}
}
- assertThat()
- 첫 번째 파라미터를 매처로 비교해서 일치하면 다음으로 넘어감. 아니면 테스트가 실패
- is()는 매처의 일종으로 equals로 비교해주는 기능을 가졌다.
- JUnit은 assertThat()을 이용해 검증을 했을 때 기대한 결과가 아니면 AssertionError를 던진다.
JUnit 테스트 실행 방법
- IDE의 기능 이용
- JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수 있다.
- 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용할 수 있다.
- 여러 개발자가 만든 코드를 모두 통합해서 테스트를 해야 할 경우
- 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트 수행
- 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 결과 통보받기
테스트 결과의 일관성
테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다.
때에 따라서는 성공해야 하는 테스트가 실패하기도 함
→ 코드에 변경사항이 없으면 테스트는 항상 동일한 결과를 내야 한다.
또한 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다.
→ JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다.
예외조건에 대한 테스트
일반적으로는 테스트 중에 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다.
→ 테스트 진행 중에 특정한 예외가 던져져야 하는 상황
→ 예외 발생 여부는 assertThat()으로는 검증이 불가능
→ 별도 처리 필요
스프링에서는 데이터 액세스 예외 클래스를 별도로 제공하고 있음 (예 : EmptyResultDataAccessException)
@Test(expected = EmptyResultDataAccessException.class) // 테스트 중에 발생할 것으로 기대하는 예외 클래스 지정
public void getUserFailure() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.get("unknown_id"); // 여기서 예외가 발생해야 한다. 예외가 발생하지 않으면 테스트가 실패한다.
}
- expected : 테스트 메소드 실행 중에 발생하리라 기대하는 예외 클래스 넣어주기
- 예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 사용 가능
항상 네거티브 테스트를 먼저 만들라.
- 부정적인 케이스 테스트부터 먼저 만드는 습관을 들이자.
- 다양한 상황과 입력값을 고려하는 포괄적인 테스트를 만들자.
- 긍정적으로 작성하면 테스트가 넘어가기 쉽다.
테스트가 이끄는 개발
테스트를 구성 → 추가하고 싶은 기능을 코드로 표현하는 과정
테스트 코드는 잘 작성된 하나의 기능정의서와도 같다.
일반적인 개발 흐름의 기능설계에 해당하는 부분을 테스트 코드가 일부분 담당할 수 있다
테스트 주도 개발(TDD, Test Driven Development)
- 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법
- 테스트 우선 개발(Test First Development)이라고도 한다.
- 개발자가 테스트를 만들어가며 개발하는 방법이 주는 장점을 극대화한 방법
- 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다.
- 이 원칙을 따라서 개발된 코드는 빠짐없이 테스트로 검증된 코드라 할 수 있음
TDD의 장점
- 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
- 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다.
- 코드에 대한 피드백을 빠르게 받아 작성한 코드에 대한 확신을 가질 수 있다.
- 자연스럽게 단위 테스트를 만들 수 있다.
- 코드를 만들어 테스트를 실행하는 사이의 간격이 매우 짧다.
- 코드의 오류가 빨리 발견되어 수정하기 좋다.
단점? 테스트를 잘 만들지 않는 이유
→ 과거에는 엔터프라이즈 애플리케이션의 테스트를 만들기가 어려웠다.
반복 작업 개선
JUnit은 테스트를 실행할 때 마다 반복되는 준비 작업을 별도에 메소드에 넣게 해 주고, 매번 테스트 메소드 실행 전에 실행하게 해 주는 기능이 있다. → @Before
@Before // @Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메소드 정의
public void setUp() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
this.dao = context.getBean("userDao", UserDao.class);
}
JUnit이 테스트 클래스를 가져와 테스트를 수행하는 방식
- 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @Before가 붙은 메소드가 있으면 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해준다.
- @After가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 모든 테스트의 결과를 종합해서 돌려준다.
@Before, @After 메소드는 테스트 메소드에서 직접 호출하지 않는다
→ 서로 주고받을 정보나 오브젝트가 있으면 인스턴스 변수를 이용해야 한다.
각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.
한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다.
→ @Test 어노테이션이 붙은 메소드의 개수만큼 인스턴스가 생성된다.
테스트 메소드를 실행할 때 마다 새로운 오브젝트를 만드는 이유
→ 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장해주기 때문
픽스처
- 테스트를 수행하는 데 필요한 정보나 오브젝트
- 여러 테스트에서 반복적으로 사용되므로 @Before 메소드를 이용해 생성