일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 11723
- 징검다리
- 이진검색
- 가장먼노드
- Spring이란
- 이진탐색
- 카카오
- @Profile
- 구현
- 토비의스프링
- 카카오인턴
- 쇠막대기 문제
- 플로이드와샬
- 프로그래머스
- bitmasking
- BinarySearch
- Spring
- 전화번호 목록
- 스프링
- Singtone
- 그래프
- Java
- 자바
- 스프링이란
- Algorithm
- sope
- 플로이드워셜
- 스프링프로젝트 시작하기
- 알고리즘
- 백준
- Today
- Total
육감적 코딩
토비의스프링3.1 [2장] 테스트 본문
2장 테스트
2.1 UserDaoTest 다시보기
2.1.1 테스트의 유용성
테스트는 내가 예상하고 의도했던 대로 코드가 정확인 동작하는를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.
2.1.2 UserDaoTest의 특징
public class UserDaoTest {
public static void main(String args[]) throws 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("married");
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를 직접 호출해서 사용한 점이다.
웹을 통한 DAO 테스트 방법의 문제점
- 테스트를 위해 기본적으로라도 웹 애플리케이션을 만들어야한다. -> 번거롭고 비효율적.
- 테스트용 웹 애플리케이션을 만들었다고 하더라도, DAO에서 문제가 아닌 웹 애플리케이션에서 문제가 발생한다면 이를 찾기 어려움.
작은 단위의 테스트
테스트는 대상에만 집중해서 테스트하는 것이 바람직하다.
따라서, 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
UserDao테스트는 한 가지 관심에 집중할 수 있게 작은 단위로 만들어진 테스트다.
테스트를 진행할 때 웹 인터페이스나, 그것을 위한 MVC 클래스, 서비스 오브젝트 등이 필요 없다. 서버에 배포할 필요도 없다.
만약 문제가 생긴다면 이는 UserDao의 코드나 아니면 DB 연결 방법 정도에서 문제가 있는 것이니 원인을 빠르게 찾을 수 있다.
이렇게 작은 단위의 코드에ㅜ 대해 테스트를 하는것을 단위 테스트 라고한다.
Why? 작은 단위로 나누는 이유 |
- 쉽게 개발자가 설계한 코드가 제대로 동작하는지 빠르게 확인하고 원인을 제한하는 데에 있다. |
자동수행 테스트 코드
매번 User의 값을 입력받는 일은 번거로운 일이다.
UserDaoTest는 main()메소드에서 간단한 방법으로 테스트의 전 과정이 자동으로 진행된다.
테스트는 이렇게 자동으로 수행되도록 만드는 것이 중요하다.
2.1.3 UserDaoTest의 문제점
-
수동확인 작업의 번거로움 : 콘솔에 나온 값을 보고 결과를 확인하는 일은 사람의 책임
-
실행 작업의 번거로움 : 아무리 간단히 실행이 가능해도 매번하기에는 번거롭다. 더 체계적으로 테스트를 실행하고 그결과를 확인하는 방법이 절실하다.
2.2 UserDaoTest 개선
2.2.1 테스트 검증의 자동화
첫 번째 문제점인 add()에 전달한 User 오브젝트에 담긴 사용자 정보와 get()을 통해 DB에서 가져온 User 오브젝트의 정보가 서로 일치하는가.
테스트 결과는 성공과 실패를 가질수있다. 실패는 테스트가 진행되는 동안에 에러가 발생해서 실패하는 경우와 테스트 작업 중에 에러가 발생하지 않았지만 그 결과가 기대한 것과 다르게 나오는 경우로 구분해볼 수 있다. 전자를 테스트에러, 후자를 테스트실패로 구분.
if (!user.getName().equals(user2.getName())){
System.out.println(“테스트 실패 (name)”);
}
else if (!user.getPassword().equals(user2.getPassword())){
System.out.println(“테스트 실패 (password)”);
}
else{
System.out.println(“조회 테스트 성공”);
}
처음 add()에 전달한 User오브젝트와 get()을 통해 가져오는 User오브젝트의 값을 비교.
이렇게 해서 테스트의 수행과 테스트 값 적용, 결과 검증까지 모두 자동화 했다.
코드변경이후에도 테스트를 빠르게 할 수 있다는 장점이있다.
"테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것"
2.2.2 테스트의 효율적인 수행과 결과 관리
main() 메소드에서 테스트하기에는 한계가 있다.
ex) 일정한 패턴을 가진 테스트, 많은 테스트, 테스트 결과 종합을 하기엔 귀찮다.
-
JUnit 테스트로 전환
개발자가 만든 클래스의 오브젝트를 생성하고 실행하는 일은 프레임워크에 의해 진행된다. 따라서 프레임워크에서 동작하는 코드는 main()메소드도 필요없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.
-
테스트 메소드 전환
Junit 프레임워크가 요구하는 조건 첫 번째, 메소드가 public으로 선언돼야 하는것.
두번째, @Test 애노테이션을 붙여주는 것.
2-4 Junit 프레임워크에서 동작할 수 있는 테스트 메소드로 전환
public class UserDaoTest{
@Test // Test 애노테이션
public void addAndGet() throws SQLException{ // public 만을 허용!
ApplitcationContext context = new ClassPathXmlApplitcationContext(“applicationContext.xml”);
UserDao dao = context.getBean(“userDao”,UserDao.class);
...
}
}
검증 코드 전환
if/else 문장을 Junit이 제공하는 방법을 이용해 전환해보자.
if (!user.getName().equals(user2.getName())){...}
assertThat(user2.getName(), is(user.getName()));
if -> assertThat
assertTaht() ? |
assertTaht() 메소드는 첫 번째 파라미터의 값을 뒤에 나오는 매처라고 불리는 조건으로 비교하는것. is() 는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다. |
2.3 개발자를 위한 테스팅 프레임워크 JUnit
2.3.1 JUnit 테스트 실행 방법
이후 테스트검증을위해
해당 부분부터는 Springboot 프로젝트를 이용하였습니다.
class 우클릭 -> Go to -> test -> Create new ...
사용하면 자동으로 Test 클래스가 생성됩니다.
2.3.2 테스트 결과의 일관성
아직도 좀 더 개선했으면하는 아쉬운점이있다.
가장 불편한 점은.
매번 UserDaoTest 테스트를 실행하기 전에 DB의 모든 USER 테이블 데이터를 모두 삭제해줘야 할 때였다.
이를 위해 addAndGet() 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서, 테스트를 수행하기 이전 상태로 만들어주는 것이다.
delteAll()의 getCount()추가
public void deleteAll() throws SQLException{
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(“delete from users”);
ps.excuteUpdate();
ps.close();
c.close();
}
public int getCount() throws SQLException{
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement(“select count(*) from users”);
ResultSet rs = ps.excuteQuery();
rs.next();
int count = rs.getInt(1);
rs.close();
ps.close();
c.close();
return count;
}
deletAll()과 getCount() 메소드의 기능은 독립적으로 자동 실행 되는 테스트는 만들기 좀 애매하다.
새로운 테스트를 만들어도 되지만, 테이블에 수동으로 데이터를 넣고 이를 확인하기에는 사람이 테스트에 참여해야 하니 자동화되는 테스트 방법은 아니다.
기존의 addAndGet() 메소드에 추가한다.
이전 테스트에서는 매번 데이터를 삭제해야 했지만, 이제는 그런 번거로운 과정이 필요없어졌다.
단위 테스트는 코드가 바뀌지않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.
2.3.3 포괄적인테스트
두 개 이상의 레코드를 add()했을 때 getCount()의 결과가 어떻게 될까?
0,1를 해봤으니 당연히 잘될거라고 생각할 수 있으나 미처 생각하지 못한 문제가 있을 수도있으니
꼼꼼히 테스트 해야한다.
addAndGet()테스트 보완
get()이 파라미터로 주어진 id에 해당하는 사용자를 가져온것인지, 그냥 아무거나 가져온 것인지 테스트에서 검증하지 못했다. 그래서 get()메소드에 대한 테스트 기능을 좀 더 보완할 필요가있다.
get() 예외조건에 대한 테스트
get()메소드에 id값이 사용자 정보가 없다면 어떻게 될까? null값을 반환하는것과 id값을 찾을 수없다는 예외를 던지는것이 있다. 여기서는 후자의 방법을 해보자.
일단 스프링의 EmptyResultDataAccessException 예외를 이용하겠다.
하지만 일반적으로 테스트 중에 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다. 이는 검증 실패는 아니고 테스트 에러라고 볼 수 있다.
그러기 때문에 assertThat() 메소드로는 검증이 불가능하다.
expected는 테스트 메소드 실행 중에 발생하리라 기대하는 예외 클래스를 넣어주면된다.
기존의 테스트와 달리 해당 예외가 던져져야 성공한다.
포괄적인테스트
개발자가 테스트를 직접 만들 때 자주하는 실수가 하나 있다. 바로 성공하는 테스트만 골라서 만드는 것이다. 그래서 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.
2.3.4 테스트가 이끄는 개발
작업한 순서를 잘 생각해보면 새로운 기능을 넣기 위해 UserDao코드를 수정하고, 그런 다음 수정한 코드를 검증하기 위해 테스트를 만드는 순서로 진행한 것이 아니다. 반대로 테스트를 먼저 만들어 테스트가 실패하는 것을 보고 나서 UserDao의 코드에 손을 대기 시작했다.
이런 순서를 따라서 개발을 진행하는 구체적인 개발 전략이 실제로 존재한다.
테스트 주도 개발
만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 개발 방법이 테스트 주도 개발(TDD) 라고 한다.
TDD의 기본원칙
-
실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다.
TDD는 아예 테스트를 먼저 만들고 -> 성공하도록 개발 하기때문에 테스트를 꼼꼼히 만들수 있다.
이미 테스트 코드를 만들어뒀기 때문에 코드를 작성하면 바로바로 실행해볼 수 있기때문에 테스트를 수행할 때까지 걸리는 시간은 0에 가깝다.
테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 한 짧게 가져가도록 권장한다. 테스트없이 오랜 시간 동안 코드를 만들고 나서 테스트를 하면, 오류가 발생했을 때 원인을 찾기가 쉽지 않다.
2.3.5 테스트 코드 개선
중복된 코드는 별도의 메소드로 뽑아내는 것이 가장 손쉬운 방법이다.
(Autowired 로 객체를 주입받아서 그냥 db를 비우는 메소드를 넣었다.)
Junit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
-
테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
-
테스트 클래스의 오브젝트를 하나 만든다.
-
@Befor가 붙은 메소드가 있으면 실행한다.
-
@Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
-
@After가 붙은 메소드가 있으면 실행한다.
-
나머지 테스트 메소드에 대해 2~5번을 반복한다.
-
모든 테스트의 결과를 종합해서 돌려준다.
※기억해야할 점
-
@Befor @After 메소드를 테스트 메소드에서 직접 호출하지 않기 때문에 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야한다.
-
각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로만든다는 점이다.
→ 이는 효율적이지 않다고 생각할 수도 있으나, 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해 주기 위해 매번 새로운 오브젝트를 만들게했다.
= 새로운 테스트가 실행될 때는 새로운 인스턴스 변수가 만들어져서 초기화 되기때문.
픽스처
테스트를 수행하는데 필요한 정보나 오브젝트를 픽스처라고 한다.
일반적으로 반복되기 때문에 @Before 메소드를 이용해 생성해두면 편리하다.
User 픽스처로 재구성한 테스트 코드
2.4 스프링 테스트 적용
@Before 메소드가 테스트 메소드 개수만큼 반복되기 때문에 애플리케이션 컨텍스트도 세 번 만들어진다. 지금은 설정도 간단하고 빈도 몇 개 없어서 별문제 아닌 듯하지만, 빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다.
JUnit은 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다. 이 메소드에서 애플리케이션 컨텍스트를 만들어 스태틱 변수에 저장해두고 테스트 메소드에서 사용할 수 있다. 하지만 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 편하다.
2.4.1 테스트를 위한 애플리케이션 컨텍스트 관리
스프링 테스트 컨텍스트 프레임워크 적용
ApplicationContext context = new GenericXmlApplicationContext(“applicationContext.xml”);
를 제거한 뒤,
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location=”/applicationContext.xml”)
public class UserDaoTest{
@Autowired
private ApplicationContext context;
…
@Before
public void setUp(){
this.dao = this.context.getBean(“userDao”,UserDao.class);
…
}
아무런 문제없이 성공할 것이다.
@RunWith 는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션이다.
SpringJUnit4ClassRunner 라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@RunWith 애노테이션은 각각의 테스트 별로 객체가 생성되더라도 싱글튼의 ApplicationContext를 보장한다.
테스트 메소드의 컨텍스트 공유
context는 동일!!
첫 번째 테스트가 실행될 때 최초로 애플리케이션 컨텍스트가 처음 만들어지면서 가장 오랜 시간이 소모되고, 그 다음부터는 이미 만들어진 애플리케이션 컨텍스트를 재사용할 수 있기 때문에 테스트 실행 시간이 매우 짧아진다.
테스트 클래스의 컨텍스트 공유
여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.
→ 수백개의 테스트 클래스가 있더라도 모두 같은 설정파일을 사용한다면 테스트 전체에 걸쳐 단 한개의 애플리케이션 컨텍스트만 만들어져 사용된다.
@Autowired
@Autowired 가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입한다. 또 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있는데, 이런 방법을 타입에 의한 자동와이어링이라고 한다.
2.4.2 DI와 테스트
UserDao와 DB 커넥션 생성 클래스 사이에는 DataSource라는 인터페이스를 뒀다.
하지만 어떤 경우 “우리는 절대로 DataSource의 구현 클래스를 바꾸지 않을 것이다.”
우리는 시스템 운영 중에 항상 SimpleDiverDataSource를 통해서만 DB 커넥션을 가져올 것이다.
굳이 DataSource 인터페이스를 사용하여 DI를 주입하는 식으로 해야하는가 ?
그래도 인터페이스를 두고 DI를 적용해야 한다.
-
소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
-
클래스의 구현방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
(1장 DB커넥션의 개수를 카운팅하는 부가기능 같은 것) -
테스트 때문이다.
단지 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야한다.
테스트를 위한 세가지 DI방법
테스트 코드에 의한 DI
테스트에 운영용DB를 사용하면 절대 안되기때문에, 테스트용 디비를 써야한다.
그렇다고 ApplicationContext.xml설정을 개발자가 테스트할 때는 테스트용 DB를 이용하도록 DataSource를 수정했다가, 서버에 배치할 때는 다시 운영용 DB를 사용하는 DataSource 바꿔주는 방법도 있겠지만, 번거롭기도 하고 위험할 수도있다.
@DirtiesContext 애노테이션은 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에 알려준다.
@DirtiesContext
public class UserDaoTest{
@Autowired
UserDao dao;
@Before
public void setUp(){
…
DataSource datasource = new SingleConnectionDataSource(
“jdbc:mysql://localhost/testdb”, “spring”, “book”, true);
dao.setDataSource(dataSource); // 코드에 의한 수동DI
}
이렇게 준비된 DataSource 오브젝트를 생성하고, 애플리케이션 컨텍스트에서 가져온
dao 오브젝트를 이용해 DI 해줄수 있다.
테스트를 위한 별도의 DI 설정
테스트 코드에서 빈 오브젝트에 수동으로 DI하는 방법은 장점보다 단점이 많다.
-
코드가 많아져 번거롭다.
-
애플리케이션 컨텍스트도 매번 새로 만들어야 하는 부담
그래서 아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용해도 된다.
두 가지 종류의 설정파일을 만들어서 하나는 운영 서버에서, 다른 하나는 테스트에 사용하면 된다.
<bean id=”dataSource”
class = “org.springframework.jdbc.datasource.SimpleDriverDataSource”>
<property name=”driverClass” value=”com.mysql.jdbc.Driver”/>
<property name=”url” value=”jdbc:mysql://localhost/testdb”/>
<property name=”username” value=”spring”/>
<property name=”password” value=”book”/>
</bean>
그리고 UserDaoTest 의 @ContextConfiguration 애노테이션에 있는 location 의 값을 바꿔준다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location=”/test-applicationContext.xml”)
public class UserDaoTest {
@DirtiesContext도 필요 없다
컨테이너 없는 DI테스트
마지막으로 스프링 컨테이너를 사용하지 않고 테스트를 만드는 방법이다.
public class UserDaoTest{
UserDao dao;
…
@Before
public void setUp(){
…
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource(
“jdbc:mysql://localhost/testdb”, “spring”, “book”, true);
dao.setDataSource(dataSource);
}
@Autowried도 없고 오브젝트 생성, 관계설정 등을 직접 해준다.
테스트를 위한 DataSource를 직접 만드는 번거로움이 있지만 애플리케이션 컨텍스트를 아예 사용하지 않으니 코드는 간단해진다. 하지만 JUnit은 매번 새로운 테스트 오브젝트를 만들기 때문에 매번 새로운 UserDao오브젝트가 만들어진다는 단점도 있다.
침투적 기술과 비침투적 기술 |
침투적 기술은 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API가 등장하거나, 이는 코드가 해당 기술에 종속됨을 의미. 반면 비침투적인 기술은 기술에 종속적이지 않은 순수한 코드를 유지할 수 있게 해준다. 스프링은 이런 비침투적인 기술의 대표적인 예다. |
“이제까지 테스트가 불편하게 설계된 좋은 코드를 본 기억이 없다.”
DI를 이용한 테스트 방법 선택
세 가지 방법중에 어떤 것을 선택해야 할까? 모두 상황에 따라 유용하게 쓸 수 있다.
하지만 스프링 컨테이너 없이 사용하는 방법을 가장 우선적으로 고려하자.
이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우
→
스프링의 설정을 이용한 DI방식의 테스트를 이용하면 편하다. 테스트에서 애플리케이션 컨텍스트를 사용하는 경우에는 테스트 전용 설정파일을 따로 만들어 각 다른 설정파일을 만들어 사용하는 경우가 일반적이다.
테스트 설정을 따로 만들었다고 하더라도 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우
→
이때는 컨텍스트에서 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI해서 테스트하는 방법을 사용하면 된다.
'정리 > Spring' 카테고리의 다른 글
토비의스프링3.1 [7장] 7.5 DI를 이용해 다양한 구현 방법 적용하기 (0) | 2020.07.29 |
---|---|
[Spring] 3. @Autowired (0) | 2020.07.23 |
[Spring] 2. ApplicationContext와 다양한 빈 설정 방법 (0) | 2020.07.22 |
[Spring] 1. IoC컨테이너와 빈 (0) | 2020.07.20 |
토비의스프링3.1 [6장] 6.5 스프링 AOP (0) | 2020.07.13 |