토비의 스프링 테스트

0_0_yoon·2022년 10월 16일
0
post-thumbnail

이번 기회에 스프링으로 프로젝트를 하면서 스프링 테스트에 대해 정리하고자 토비의 스프링 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("password");

		dao.add(user);
		
        System.out.println(user.getId() + "등록 성공");
        
		User user2 = dao.get(user.getId());
		System.out.println(user2.getName());
        System.out.println(user.getPassword());
                
        System.out.println(user2.getId() + "조회 성공");
	} 		
}
  • main 메서드를 통해서 테스트를 진행한다.
  • 테스트 결과를 콘솔로 출력한다.

웹을 통한 DAO 테스트 방법의 문제점

MVC 프레젠테이션 계층을 구현한다. 테스트용 웹 애플리케이션을 서버에 띄우고 요청을 보내본다.

이 방법은 배보다 배꼽이 더 크다;

  1. DAO 테스트를 위해 다른 계층을 구현해야 한다.
  2. 디버깅하기 힘들다.
    1. 하나의 테스트를 수행하는 데 참여하는 클래스와 코드가 너무 많기 때문이다.
    2. 심지어 서버의 설정 상태까지 모두 테스트에 영향을 끼친다.

작은 단위의 테스트

개념

여기서 단위는 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위이다. 크게는 사용자 관리 기능을 모두 통틀어서 하나의 단위로 볼 수 있고, 작게 보자면 메서드 하나만 가지고 하나의 단위로 볼 수 있다. 일반적으로 단위는 작을수록 좋다.

외부의 리소스에 의존하는 테스트는 단위 테스트가 아니다.

Dao 단위 테스트의 경우에 DB에 의존적인, 즉 테스트마다 DB가 격리되지 않는다면 단위 테스트라고 보기 어렵다.

필요성

  1. 예외가 발생해도 그 이유를 찾는 데 많은 시간이 걸릴 수 있다.
  2. 빠르게 피드백 받을 수 있다.
  3. 해당 기능에 확신을 갖을 수 있다.

자동수행 테스트 코드

테스트가 코드를 통해 자동으로 실행된다. 즉 UserDaoTest 를 실행하면 자동으로 테스트가 실행된다. 만약 앞서 나온 웹을 통해 테스트를 수행한다면 코드가 아닌 개발자 스스로가 테스트를 수행해야한다.

자동수행 테스트 코드의 장점은 자주 반복할 수 있다는 것이다.

지속적인 개선과 점진적인 개발을 위한 테스트

작은 단계를 거치는 동안 테스트를 수행해서 확신을 가지고 코드를 변경해갔기 때문에 전체적으로 코드를 개선하는 작업에 속도가 붙고 더 쉬워졌다.

또한 UserDao의 기능을 추가하려고 할 때도 미리 만들어둔 테스트 코드는 유용하게 쓰일 수 있다. 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.

UserDaoTest의 문제점

  1. 수동 확인 작업의 번거로움
  2. 실행 작업의 번거로움

UserDaoTest 개선

테스트 검증의 자동화

// 수정 전 테스트 코드
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + "조회 성공");
// 수정 후 테스트 코드
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("조회 테스트 성공");
}

테스트의 효율적인 수행과 결과 관리

Main 메서드로 만든 테스트의 한계를 극복하기 위해서 JUnit 사용

  1. 일정한 패턴을 가진 테스트를 만들 수 있다.
  2. 많은 테스트를 간단히 실행시킬 수 있다.
  3. 테스트 결과를 종합해서 볼 수 있다.
  4. 테스트가 실패한 곳을 빠르게 찾을 수 있다.

JUnit 테스트로 전환

JUnit 은 프레임워크이다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다.

테스트 메소드 전환

main 메서드는 제어권을 직접 갖는다는 의미이다. JUnit 프레임워크를 사용하기 위해서 일반 메서드로 옮겨야한다. 이때 두 가지 조건이 붙는다.

  1. 메소드가 Public 으로 선언돼야 한다.(이제는 default 접근자도 사용 가능함)
  2. @Test 어노테이션을 붙여줘야한다.
public class UserDaoTest {
	
	@Test 
	public void andAndGet() throws SQLException {
		ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
        
		UserDao dao = context.getBean("userDao", UserDao.class);
		
		// 생략
	}
}

검증 코드 전환

if (!user.getName()).equals(user2.getName()){...}

// 이 if 문장의 기능을 JUnit 이 제공해주는 assertThat 이라는 스태틱 메서드를 이용해 다음과 같이 변경할 수 있다.

assertThat(user2.getName()).is(user.getName()));

JUnit은 예외가 발생하거나 assertThat 메서드에서 실패하지 않고 테스트 메서드의 실행이 완료되면 테스트가 성공했다고 인식한다.

JUnit 테스트 실행

import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
	JUnitCore.main("springbook.user.dao.UserDaoTest");
}

개발자를 위한 테스팅 프레임워크 JUnit

JUnit 테스트 실행 방법

IDE

IDE에 내장된 JUnit 테스트 지원 도구를 사용한다.

빌드 툴

개발자 개인별로는 IDE 에서 JUnit 도구를 활용해 테스트를 실행하는 게 가장 편리하다. 그런데 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때도 있다. 이런 경우에는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다. 이때는 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 그 결과를 메일 등으로 통보받는 방법을 사용하면 된다.

테스트 결과의 일관성

코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.

deleteAll 메서드를 만들어서 테스트가 등록한 사용자 정보를 삭제하도록 구현함 → 매번 수동으로 DB 데이터를 지워주는 부분을 자동화함

getCount 메서드를 만들어서 USER 테이블의 레코드 개수를 조회한다 → 위에서 만든 deleteAll 이 잘 동작했는지 검증한다.

@Test
public void addAndGet() throws SQLException {
	...
    
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));
}
User user = new User();
user.setId("id");
user.setName("칙촉");
user.setPassword("password");

dao.add(user);
assertThat(dao.getCount(), is(1));

User user2 = dao.get(user.getId());

assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword(), is(user.getPassword()));

동일한 결과를 보장하는 테스트

위의 코드와 다르게 테스트를 마치기 직전에 데이터를 지워도 동일한 테스트 결과를 얻을 수 있다. 하지만 해당 테스트 실행 이전에 다른 이유로 USER 테이블에 데이터가 들어가 있다면 테스트가 실패할 수도 있다.

스프링은 DB를 사용하는 코드를 테스트하는 경우 매우 편리한 테스트 방법을 제공해준다.

@DataJpaTest, @JdbcTest 는

@Transactional 어노테이션이 있어서 자동으로 롤백시켜준다.(테스트 코드에서 @Transactional 어노테이션이 있으면 select 이외의 모든 쿼리는 롤백 대상이된다)

테스트 주도 개발

실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다. → 오버엔지니어링을 방지할 수 있다.

테스트가 실패하면 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다 → 즉각적인 피드백을 받을 수 있다.

중요성 코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아진다.또, 작성한 코드가 많기 때문에 무엇을 테스트해야 할지 막막할 수도 있다.

테스트 코드 개선

JUnit이 테스트를 수행하는 방식

  1. 테스트 클래스에서 @Test 가 붙은 public 이고 void 형이며 파라미터가 없는 테스트 메서드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다.
  3. @Before 가 붙은 메서드가 있으면 실행한다.
  4. @Test 가 붙은 메서드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After 가 붙은 메서드가 있으면 실행한다.
  6. 나머지 테스트 메서드에 대해 2~5 번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.

각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다. 덕분에 멤버 필드도 부담없이 사용할 수 있다.

픽스처

테스트 클래스에서 자주 사용되는 값, 오브젝트들은 멤버필드로 선언해서 사용한다. 어차피 매번 새로운 테스트 오브젝트가 만들어지니까 멤버필드에서 바로 초기화해도 상관없다. 하지만 픽스처 생성 로직이 흩어져 있는 것 보다는 모여 있는 편이 나을 테니 @Before 메서드를 이용

public class UserDaoTest {
	private UserDao dao;
    private User user1;
    private User user2;
    private User user3;
    
    @Before
    public void serUp() {
    	...
        this.user1 = new User("id1", "영", "password");
        this.user2 = new User("id2", "윤", "password");
        this.user3 = new User("id3", "안", "password");
    }
    ...
}

스프링 테스트 적용

현재 테스트 코드에는 몇가지 문제가 있다. ApplicationContext 를 여러번 만들고 있다. 추후에 빈이 많아지고 복잡해지면 ApplicationContext 생성에 적지 않은 시간이 걸릴 수 있다. ApplicationContext 만들어질때 모든 싱글톤 빈 오브젝트를 초기화하기 때문이다.

또 한 가지 문제는 ApplicationContext가 초기화될 떄 어떤 빈은 독자적으로 많은 리소스를 할당하거나 독립적인 스레드를 띄우기도 한다는 점이다. 이런 경우에는 테스트를 마칠 때마다 ApplicationContext 내의 빈이 할당한 리소스 등을 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 ApplicationContext가 만들어지면서 문제를 일으킬 수도 있다.

→ @BeforeClass 를 사용해서 스태틱 필드에 ApplicationContext를 저장한다. @BeforeClass 는 테스트 클래스 전체에 걸쳐 딱 한 번만 실행된다. But 더 편리하게 할 수 있다.

테스트를 위한 ApplicationContext 관리

스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 ApplicationContext를 만들어서 모든 테스트가 공유하게 할 수 있다.(추가 ApplicationContext는 초기화할 때 자기 자신도 빈으로 등록한다. 그래서 Autowired 를 통해 주입받을 수 있는것)

@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이 테스트를 진행하는 중에 테스트가 사용할 ApplicationContext를 만들고 관리하는 작업을 진행해준다.

@ContextConfiguration: 자동으로 만들어줄 ApplicationContext 설정파일 위치를 지정한 것이다.

결과: 하나의 테스트 클래스 내의 테스트 메소드는 같은 ApplicationContext를 공유해서 사용할 수 있게 됨

(추가: 최초 ApplicationContext가 초기화될때 bean 이 초기화되는게 아니라 getBean을 호출할때 초기화된다. )

테스트 클래스의 컨텍스트 공유

여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 ApplicationContext를 사용한다면, 스프링은 테스트 클래스 사이에서도 ApplicationContext를 공유하게 해준다. → 테스트 전체에 걸쳐서 단 한개의 ApplicationContext만 만들어져 사용됨, 테스트 성능이 대폭 향상됨

@Autowired

별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있다.(추가: 타입이 여러개라면 변수의 이름과 같은 이름의 빈이 있는지 확인한다, 변수 이름으로도 찾을수 없을 경우 예외 발생) ApplicationContext가 가지고 있는 빈을 DI 받을수 있다면 굳이 ApplicationContext를 주입 받는게 아니라 우리가 사용할 UserDao를 직접 DI 받도록 함

테스트 코드에 의한 DI

테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들어서 사용함, applicationContext.xml 파일의 설정정보를 따라 구성한 오브젝트를 가져와 의존관계를 강제로 변경

@DirtiesContext
public class UserDaoTest {
	@Autowired
    UserDao userDao;
    
    @Before
    public void setUp() {
    	...
        DataSource dataSource = new SingleConnectionDataSource(
        	"jdbc:mysql://localhost/testdb", "spring", "book", true
        );
    }
    ...
}
  //  코드에 의한 수동 DI
  dao.setDataSource(dataSource);

But DirtiesContext 는 매번 ApplicationContext를 새로 만듦!

메서드 레벨의 @DirtiesContext 사용하기

@DirtiesContext 는 클래스에만 적용할 수 있는 건 아니다. 하나의 메서드에서만 컨텍스트 상태를 변경한다면 메서드 레벨에 @DirtiesContext 를 붙여주는 편이 낫다. 해당 메서드의 실행이 끝나고 나면 이후에 진행되는 테스트를 위해 변경된 애플리케이션 컨텍스트는 폐기되고 새로운 애플리케이션 컨텍스트가 만들어진다.

테스트를 위한 별도의 DI 설정

아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용.

컨테이너 없는 DI 테스트

스프링 컨테이너 없이 테스트 코드의 수동 DI 만을 이용

public class UserDaoTest {
	// @Autowired 가 없다
	UserDao dao;
    ...
    
    @Before
    public void setUp() {
    	...
        // 객체 생성 및 관계설정을 직접 해준다
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource(
        	"jdbc:mysql://localhost/testdb", "spring", "book", true
        );
        dao.setDataSource(dataSource);
    }
}

침투적 기술과 비침투적 기술

침투적 기술은 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API 가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술을 말한다. 침투적 기술을 사용하면 애플리케이션 코드가 해당 기술에 종속되는 결과를 가져온다. 반면에 비침투적인 기술은 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다. 따라서 기술에 종속적이지 않은 순수한 코드를 유지할 수 있게 해준다. 스프링은 이런 비침투적인 기술의 대표적이 예다. 그래서 스프링 컨테이너 없는 DI 테스트도 가능한 것이다.

DI 를 이용한 테스트 방법 선택

컨테이너 없는 DI 테스트

항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.

@RunWith, @ContextConfiguration

여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우

@DirtiestContext

예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우

학습 테스트로 배우는 스프링

학습 테스트의 장점

  1. 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
  2. 학습 테스크 코드를 개발 중에 참고할 수 있다.
  3. 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
  4. 테스트 자성에 대한 좋은 훈련이 된다.
  5. 새로운 기술을 공유하는 과정이 즐거워진다.

예시:

public class JUnitTest {
	static JUnitTest testObject;
    
    @Test
    public void test1() {
    	assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }
    
    @Test
    public void test2() {
    	assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }
    
    @Test
    public void test3() {
    	assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }
}

sameInstatnce: 동일성을 비교한다.
예시의 결과로 알 수 있는점은 JUnit 이 @Test 메서드 마다 객체를 생성한다는 것이다.

버그 테스트

코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다.

QA 과정, 또는 사용자 버그가 발생한 경우에 무턱대로 코드를 뒤져가면서 수정하려고 하기보다는 먼저 버그 테스트를 만들어보는 편이 유용하다.

장점

  1. 테스트의 완성도를 높여준다.
  2. 버그의 내용을 명확하게 분석하게 해준다.
  3. 기술적인 문제를 해결하는 데 도움이 된다.

동등분할

같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법을 말한다. 어떤 작업의 결과의 종류가 true, false 또는 예외발생 세 가지라면 각 결과를 내는 입력 값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는 것이 좋다.

경계값 분석

에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다. 보통 숫자의 입력 값인 경우 0 이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.

정리

  • 테스트는 자동화돼야하고, 빠르게 실행할 수 있어야 한다. → 검증을 개발자가 아닌 로직하도록 함, 웹 테스트 보다는 작은 단위로 테스트해서 즉각적인 피드백을 받도록 한다.
  • main 메서드 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다. → 여러 편의 메서드를 지원한다.
  • 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안 된다. → DB 데이터를 완전 지워줌 → 테스트 DB 분리
  • 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁠 수 있다.
  • 코드 작성과 테스트 수행 간격이 짧을수록 효과적이다.
  • 테스트하기 쉬운 코드가 좋은 코드다. → 하나의 기능만을 가진 코드를 작성
  • 테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유용하다.
  • 테스트 코드도 애플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다.
  • @Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다.
  • 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다.
  • 동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유한다.
  • @Autowired 를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있다.
  • 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하자.
  • 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다.

세줄 정리

  • spring 사용자들아 웹 서비스 만들때 웹 테스트 같이 덩어리로 테스트 하지말고 좀 더 작은 단위로 테스트해라.

  • 단위 테스트할때 JUnit 프레임워크 써.

  • TDD 한번 해봐.

profile
꾸준하게 쌓아가자

0개의 댓글