이 글은 “토비의 스프링” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “gunjuko92@gmail.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

테스트

스프링이 개발자에게 제공하는 가장 중요한 가치는 객체지향과 테스트이다. 애플리케이션은 계속 변하고 복잡해져 간다. 그 변화에 대응하는 첫 번째 전략이 확장과 변화를 고려한 객체지향적 설계와 그것을 효과적으로 담아낼 수 있는 IoC/DI 같은 기술이라면, 두 번째 전략은 만들어진 코드를 확신할 수 있게 해주고, 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술이다.

2장에서는 테스트란 무엇이며 그 가치와 장점, 활용 전략, 스프링과의 관계를 살펴본다. 그리고 이 책에서 활용할 대표적인 테스트 프레임워크를 소개하고, 이를 이용한 학습 전략도 알아볼 것이다.

1. UserDaoTest 다시보기

1.1 테스트의 유용성

테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 또한 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.

1.2 UserDaoTest의 특징

아래는 main() 메소드를 이용해서 작성한 테스트 코드이다.

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.setld("user"); 
		user.setName("백기선"); 
		user.setPassword("married");
		
		dao .add(user);
		
		System.out.println(user.getId() + " 등록 성공“);
		User user2 =dao.get(user.getld()); 
		System.out.println(user2.getName()); 
		System.out.println(user2.getPassword());
		System.out.println(user2.getld() + " 조회 성공");
	}
}
  • 자바에서 가장 손쉽게 실행 가능한 main() 메소드를 이용한다.
  • 테스트 할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출한다.
  • 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에서 만들어 넣어준다.
  • 테스트의 결과를 콘솔에 출력해준다.
  • 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지로 출력해준다.
작은 단위의 테스트

테스트 하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트히는 것이 바람직하다. 한꺼번에 너무 많은 것을 몰아서 테스트하면 테스트 수행 과정도 복잡해지고, 오류가 발생했을 때 정확한 원인을 찾기가 힘들어진다. 따라서 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다. 관심사의 분리라는 원리가 여기에도 적용된다. 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다.

작은 단위의 모드에 대해 테스트를 수행한 것을 단위 테스트(unit test)라고 한다. 여기서 밀하는 단위란 무엇인지, 그 크기와 범위가 어느 정도인지 딱 정해진 건 아니다. 크게는 사용자 관리 기능을 모두 통틀어서 하나의 단위로 볼 수도 있고, 작게 보자면 UserDao의 add() 메소드 하나만 가지고 하나의 단위라고 생각할 수도 있다. 충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위라고 보면 된다.

일반적으로 단위는 작을수록 좋다. 단위를 넘어서는 다른 코드들은 신경 쓰지 않고, 참여하지도 않고 테스트가 동작할 수 있으면 좋다. 어떤 개발자는 테스트 중에 DB가 샤용되면 단위 테스트가 아니라고도 한다. 그럼 UserDaoTest는 단위 테스트가 아니라고 봐야 할까? 그렇지는 않다. 지금까지 UserDaoTest를 수행할 때 매번 USER 테이블의 내용을 비우고 테스트를 진행했다. 이렇게 사용할 DB의 상태를 테스트가 관장하고 있다면 이는 단위 테스트라고 해도 된다. 하지만 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 한다.

단위 테스트를 하는 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서다. 이때 확인의 대상과 조건이 간단하고 명확할수록 좋다. 그래서 작은 단위로 제한해서 테스트하는 것이 편리하다. 단위 테스트는 주로 개발자가 만든 코드를 스스로 확인하기 위해 사용하는데, 그래서 이를 개발자 테스트 또는 프로그래머 테스트라고도 한다.

다시 말하지만 UserDaoTest는 UserDao라는 작은 단위의 데이터 액세스 기능만을 테스트하기 위해 만들어졌고, 그 외의 계층이 참여하지 않기 때문에 이는 분명 단위 테스트다. 또한 우리가 만들고 개선한 코드가 처음 설계하고 의도한 대로 바르게 동작했는지를 확인하기 위해 개발자 입장에서 만든 것이므로 이를 개발자 테스트라고 부를 수도 있다.

자동 수행 테스트 코드

테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다. 어떤 개발지는 모든 클래스는 스스로 자신을 테스트하는 main() 메소드를 갖고 있어야 한다고 주장하기도 한다. 굳이 모든 클래스의 mainO 메소드에 테스트 코드가 들어가 있을 필요까지는 없겠지만, 어쨌든 테스트 자체가 사람의 수작업을 거치는 방법을 사용하기보다는 코드로 만들어져서 자동으로 수행될 수 있어야 한다는 건 매우 중요하다.

자동으로 수행되는 테스트의 장점은 자주 반복할 수 있다는 것이다. 번거로운 작업이 없고 테스트를 빠르게 실행할 수 있기 때문에 언제든 코드를 수정하고 나서 테스트를 해볼 수있다.

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

일단은 단순 무식한 방법으로 정상동작하는 코드를 만들고, 그에 대한 테스트를 만든다. 그리고나면, 매우 작은 단계를 거쳐가면서 계속해서 코드를 개선해나갈 수 있다. 오히려 작은 단계를 거치는 동안 테스트를 수행해서 확신을 가지고 코드를 변경해가면 전체적으로 코드를 개선하는 작업에 속도가 붙고 더 쉬워질 수도 있다.

기능을 추가하려고 할 때도 미리 만들어둔 테스트 코드는 유용하게 쓰일 수 있다. 일단은 가장 단순한 기능을 만들고, 이를 테스트로 검증해서 만든 코드에 대한 확신을 갖는다. 그리고 거기에 조금씩 기능을 더 추가해가면서 그에 대한 테스트도 함께 추가하는 식으로 점진적인 개발이 기능해진다. 테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.

1.3 UserDaoTest의 문제점

수동 확인 작업의 번거로움

테스트 수행은 코드에 의해 자동으로 진행되긴 하지만 테스트의 결과를 확인하는 일은 사람의 책임이므로 완전히 자동으로 테스트 되는 방법이라고 말할 수가 없다.

실행 작업의 번거로움

아무리 간단히 실행 가능한 main() 메소드라고 하더라도 매번 그것을 실행하는 것은 제법 번거롭다. 만약 DAO가 수백 개가 되고 그에 대한 main() 메소드도 그만큼 만들어진다면. 전체 기능을 테스트해보기 위해 main() 메소드를 수백 번 실행하는 수고가 필요하다.

2.2 UserDaoTest 개선

모든 테스트는 성공과 실패의 두 가지 결과를 가질 수 있다. 또 테스트의 실패는 테스트가 진행되는 동안에 에러가 발생해서 실패하는 경우와, 테스트 작업 중에 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나오는 경우로 구분해볼수있다. 여기서 전자를 테스트 에러, 후자를 테스트 실패라 부른다.

자동화된 테스트를 위한 xUnit 프레임워크를 만든 켄트 벡은 “테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것”이라고 했다.

만들어진 코드의 기능을 모두 점검할 수 있는 포괄적인 테스트(comprehensive test)를 만들면 개발한 애플리케이션은 이후에 어떤 과감한 수정을 하고 나서도 테스트를 모두 돌려 보고나면 안심할 수 있다. 혹은 테스트를 통해 그 변경에 영향을 받는 부분이 정확히 확인된다면 빠르게 조치를 취할 수 있다.

이렇게 개발 과정에서 또는 유지보수를 하면서 기존 애플리케이션 코드에 수정을 할 때 마음의 평안을 얻고, 자신이 만지는 코드에대해 항상 자신감을 가질 수있으며, 새로 도입한 기술의 적용에 문제가 없는지 확인할 수 있는 가장 좋은 방법은 빠르게 실행 가능하고 스스로 테스트 수행과 기대하는 결과에 대한 확인까지 해주는 코드로 된 자동화된 테스트를 만들어두는 것이다.

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

Junit 테스트로 전환

JUnit은 프레임워크다. 1장에서 프레임워크의 기본 동작원리가 바로 제어의 역전이라고 설명했다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다. 개발자가 만든 클래스의 오브젝트를 생성하고 실행히는 일은 프레임워크에 의해 진행된다. 따라서 프레임워크에서 통작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.

테스트 메소드 전환

가장 먼저 할 일은 main() 메소드에 있던 테스트 코드를 일반 메소드로 옮기는 것이다. 새로 만들 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 두가지를 따라야 한다. 첫째는 메소드가 public으로 선언돼야 하는 것이고, 다른 하나는 메소드에 @Test라는 애노태이션을 붙여주는 것이다.

아래 코드는 Junit 프레임워크에서 동작하도록 코드를 재구성 한 것이다.

public class UserDaoTest {
    
    @Test
    pulic void addAndGet() throws Exception {
        ApplicationContext context =new GenericXmlApplicationContext("applicationContext.xml");		
		UserDao dao =context.getBean("userDao", UserDao.class);
        
        ...
    
    }
}
검증 코드

Junit에는 assertThat이라는 스태틱 메소드를 제공하는데, 이 메소드를 이용하면 값을 검증할 수 있다. assertThat() 메소드는 첫 번째 파라미터의 값을 뒤에 나오는 매처라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어준다. is()는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다.

JUnit은 예외가 발생하거나 assertThat()에서 실패하지 않고 테스트 메소드의 실행이 완료되면 테스트가 성공했다고 인식한다. JUnit은 테스트를 실행하고 나면 테스트 결과를 다양한 방법으로 알려준다.

Junit 테스트 실행

JUnit은 assertThat()을 이용해 검증을 했을 때 기대한 결과가 아니면 AssertionError를 던진다. 따라서 assertThat()의 조건을 만족하지 못하면 테스트는 더 이상 진행되지 않고 JUnit은 테스트가 실패했음을 알게 된다. 테스트 수행 중에 일반 예외가 발생한 경우에도 마찬가지로 테스트 수행은 중단되고 테스트는 실패한다.

3. 개발자를 위한 테스팅 프레임워크 Junit

스프링의 핵심 기능 중 하나인 스프링 테스트 모률도 JUnit을 이용한다. 따라서 스프링의 기능을 익히기 위해서라도 JUnit은 사용할 줄 알이야 한다. JUnit 테스트는 main() 메소드와 System.out.println()으로 만든 테스트만큼 단순하기 때문에 빠르게 작성할 수 있다. 또, 테스트 작성 시 자주 필요한 편리한 여러 가지 부가기능도 제공한다. 대부분의 자바 IDE는 JUnit 테스트를 손쉽게 실행할 수 있는 JUnit 테스트 지원 기능을 내장하고 있어서 더욱 편리하게 JUnit 테스트를 만들고 활용할 수 있게 해준다.

3.1 Junit 테스트 실행 방법

가장 좋은 JUnit 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를사용하는 것이다.

IDE

개발 중에 테스트를 실행하고자 할 때는 이클립스 같은 IDE의 지원을 받는 것이 가장 편리하다.

빌드 툴

프로젝트의 빌드를 위해 ANT나 Maven 같은 빌드 툴과 스크립트를 사용하고 었다면, 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용해 JUnit 테스트를 실행 할 수 있다. 테스트 실행 결과는 옵션에 따라서 HTML이나 텍스트 파일의 형태로 보기 좋게 만들어진다.

3.2 테스트 결과의 일관성

지금까지 테스트를 실행하면서 가장 불편했던 일은, 매번 UserDaoTest 테스트를 실행하기 전에 DB의 USER 테이블 데이터를 모두 삭제해줘야 할 때였다.

여기서 생각해 볼 문제는 테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다는 점이다. DB 서버가 다운됐다거나 네트워크에 장애가 생겨서 DB에 접근하지 못하는 예외적인 상황이라면 이해 할 수 있겠는데 지금 발생히는 문제는 별도의 준비작업 없이는 성공해야 마땅한 테스트가 실패하기도 한다는 점이다. 반복적으로 테스트를 했을 때 테스트가 실패하기도 하고 성공하기도 한다면 이는 좋은 테스트라고 할 수가 없다. 코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.

UserDaoTest의 문제는 이전 테스트 때문에 DB에 등록된 중복 데이터가 있을 수 있다는 점이다. 가장 좋은 해결책은 addAndGet() 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서 테스트를 수행하기 이전 상태로 만들어주는 것이다. 그러면 테스트를 아무리 여러 번 반복해서 실행하더라도 항상 동일한 결과를 얻을 수 있다.

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

단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다는 점을 잊어선 안된다. DB에 남아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하는 것은 물론이고, 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다.

3.3 포괄적인 테스트

테스트를 안 만드는 것도 위험한 일이지만, 성의 없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 건 더 위험하다. 특히 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. 이런 테스트는 마치 하루에 두 번은정확히 맞는다는 시계와 같을 수도 있다. 죽은 시계 말이다.

테스트 메소드는 한 번에 한 가지 검증 목적에만 충실한 것이 좋다.

JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘 못 만든 것이다. 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야한다.

예외조건 테스트

JUnit은 예외조건 테스트를 위한 특별한 방법을 제공해준다. @Test 애노테이션의 expected 엘리먼트다. expected는 테스트 메소드 실행 중에 발생하리라 기대하는 예외 클래스를 넣어주면된다.

@Test에 expected를 추가해놓으면 보통의 테스트와는 반대로 정상적으로 테스트 메소드를 마치면 테스트가 실패하고 expected에서 지정한 예외가 던져지면 테스트가 성공한다. 예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 쓸 수 있다.

포괄적인 테스트

개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공히는 테스트만 골라서 만드는 것이다. 개발자는 머릿속으로 이 코드가 잘 돌아기는 케이스를 상상 하면서 코드를 만드는 경우가 일반적이다. 그래서 테스트를 작성할 때도 문제가 될 만한 상황이나, 입력 값 등은 교묘히도 잘 피해서 코드를 만드는 습성이 있다. 이건 테스트 코드를 통한 자동 테스트뿐 아니라, UI를 통한 수동 테스트를 할 때도 빈번하게 발생하는 문제다.

조금만 신경을 쓰면 자신이 만든 코드에서 발생할 수 있는 다양한 상황과 입력 값을 고려하는 포괄적인 테스트를 만들 수 있다. 스프링의 창시자인 로드 존슨은 “항상 네거티브 테스트를 먼저 만들라”는 조언을 했다. 개발자는 빨리 테스트를 만들어 성공히는 것을 보고 다음 기능으로 나가고 싶어하기 때문에, 긍정적인 경우를 골라서 성공할 만한 테스트를 먼저 작성하게 되기가 쉽다.

테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. get() 메소드의 경우라면, 존재하는 id가 주어졌을 때 해당 레코드를 정확히 가져오는가를 테스트하는 것도 중요하지만, 존재하지 않는 id가 주어졌을 때는 어떻게 반응할 지를 먼저 결정하고, 이를 확인할 수 있는 테스트를 먼저 만들려고 한다면 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다.

3.4 테스트가 이끄는 개발

기능 설계를 위한 테스트

테스트에는 만들고 싶은 기능에 대한 조건과 행위, 결과에 대한 내용이 잘표현되어 있다.

  • 조건 : 어떤 조건을 가지고
  • 행위 : 무엇을 할 때
  • 결과 : 어떤 결과가 나온다.

이렇게 비교해보면 테스트 코드는 마치 잘 작성된 하나의 기능 정의서처럼 보인다. 그래서 보통 기능 설계, 구현, 테스트라는 일반적인 개발 흐름의 기능 설계에 해당하는 부분을 이 테스트 코드가 일부분 담당하고 있다고 볼 수도 있다. 이런 식으로 추가하고 싶은 기능을 일반 언어가 아니라 테스트 코드로 표현해서, 마치 코드로 된 설계문서 처럼 만들어 놓은 것이라고 생각해보자. 그러고 나서 실제 기능을 가진 애플리케이션 코드를 만들고 나면, 바로 이 테스트를 실행해서 설계한 대로 코드가 동작히는지를 빠르게 검증 할 수 있다.

만약 테스트가 실패하면 이때는 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다. 그리고 문제가 되는 부분이 무엇인지에 대한 정보도 테스트 결과를 통해 얻을 수 있다. 다시 코드를 수정하고 테스트를 수행해서 테스트가 성공하도록 애플리케이션 코드를 계속 다듬어간다. 결국 테스트가 성공한다면 그 순간 코드 구현과 테스트라는 두 가지 작업이 동시에 끝나는 것이다. 흥미로운 방법이 아닌가?

테스트 주도 개발

만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법이 있다. 이를 테스트 주도 개발(TDD. Test Driven Development)이라고 한다. 또는 테스트를 코드보다 먼저 작성한다고 해서 테스트 우선 개발(Test First Development)이라고도 한다.

TDD는 개발자가 테스트를 만들어가며 개발하는 방법이 주는 장점을 극대화한 방법이라고 볼 수 있다. “실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다는 것이 TDD의 기본 원칙이다. 이 원칙을 따랐다면 만들어진 모든 코드는 빠짐없이 테스트로 검증된 것이라고 볼 수 었다.

TDD는 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다. 또한 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다. 사실 코드를 만들고 테스트를 수행할 때까지 걸리는 시간은 0에 기깝다. 이미 테스트를 만들어뒀기 때문에 코드를 작성하면 바로 바로 테스트를 실행해볼 수 있기 때문이다. 그 덕분에 코드에 대한 피드백을 매우 빠르게 받을 수 있게 된다. 또, 매번 테스트가 성공하는 것을 보면서 작성한 코드에 대한 확신을 가질 수 있어 가벼운 마음으로 다음 단계로 넘어 갈 수가 있다. 한편으로는 자신감을, 다른 한편으로는 미음의 여유를 주는 방법이다.

TDD에서는 테스트 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 한 짧게 가져가도록 권장한다. 테스트를 반나절 동안이나 만들고 오후 내내 테스트를 통과시키는 코드를 만드는 식의 개발은 그다지 좋은 방법이 아니다.

TDD를 하면 자연스럽게 단위 테스트를 만들 수 있다. 빠르게 자동으로 실행 할 수 있는 단위 테스트가 아니고서는 이런 식의 개발은 거의 불가능하다.

TDD의 장점 중 하나는 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다는 점이다. 개발한 코드의 오류는 빨리 발견할수록 좋다. 빨리 발견된 오류는 쉽게 대응이 가능하기 때문이다. 테스트 없이 오랜 시간 동안 코드를 만들고 나서 테스트를 하면, 오류가 발생했을 때 원인을 찾기가 쉽지 않다.

테스트는 코드를 작성한 후에 기능한 빨리 실행할 수 있어야 한다. 그러려면 테스트 없이 한 번에 너무 많은 코드를 만드는 것은 좋지 않다. 테스트를 먼저 만들어두면 코딩이 끝나자마자 바로 테스트를 실행 할 수 있으니 가장 좋은 방법이다. 하지만 이런 방법이 불편하다면 일정 분량의 코딩을 먼저 해놓고 빠른 시간 안에 테스트 코드를 만들어 테스트 해도 상관없다.

스프링은 테스트하기 편리한 구조의 애플리케이션을 만들게 도와줄 뿐만 아니라, 엔터프라이즈 애플리케이션 테스트를 빠르고 쉽게 작성할 수 있는 매우 편리한 기능을 많이 제공한다.

3.5 테스트 코드 개선

테스트 코드도 언제든지 내부구조와 설계를 개선해서 좀 더 깔끔하고 이해하기 쉬우며 변경이 용이한 코드로 만들 필요가 있다. 테스트 코드 자체가 이미 자신에 대한 테스트이기 때문에 테스트 결과가 일정하게 유지된다면 얼마든지 리팩토링을 해도 좋다.

@Before

Junit이 제공하는 애노테이션으로 @Test 메소드가 실행되기 전에 먼저 실행돼야 하는 메소드를 정의한다.

테스트 실행 흐름

JUnit 프레임워크가 테스트 메소드를 실행하는 과정을 알아야 한다. 프레임워크는 스스로 제어권을 가지고 주도적으로 동작하고, 개발자가 만든 코드는 프레임워크에 의해 수동적으로 실행된다. 그래서 프레임워크에 사용되는 코드만으로는 실행 흐름이 잘 보이지 않기 때문에 프레임워크가 어떻게 사용할지를 잘 이해하고 있어야한다.

JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.

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

실제로는 이보다 더 복잡한데 간단히 정리하면 JUnit 테스트는 위의 7단계를 거쳐서 진행된다고 볼 수 있다.

JUnit은 @Test가 붙은 메소드를 실행하기 전과 후에 각각 @Before와 @After가 붙은 메소드를 자동으로 실행한다. 보통 하나의 테스트 클래스 안에 있는 테스트 메소드들은 공통적인 준비 작업과 정리 작업이 필요한 경우가 많다. 이런 작업들을 @Before, @After가 붙은 메소드에 넣어두면 JUnit이 자동으로 메소드를 실행해주니 매우 편리하 다. 각 테스트 메소드에서 직접 setUp( )과 같은 메소드를 호출할 필요도 없다.

한 가지 꼭 기억해야 할 사항은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 점이다. 한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다. 테스트 클래스가 @Test 테스트 메소드를 두 개 갖고 있다면, 테스트가 실행되는 중에 JUnit은 이 클래스의 오브젝트를 두 번 만들 것이다.

그림

JUnit 개발자는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다. 덕분에 인스턴스 변수도 부담 없이 사용할 수 있다. 어차피 다음 테스트 메소드가 실행될 때는 새로운 오브젝트가 만들어져서 다 초기화 될 것이다.

픽스처

테스트를 수행히는 데 펼요한 정보나 오브젝트를 픽스처(fixture)라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해 생성해두면 편리하다. UserDaoTest에서라면 dao가 대표적인 픽스처다. 테스트 중에 add() 메소드에 전달히는 User 오브젝트들도 픽스처라고 볼 수 었다.

4. 스프링 테스트 적용

테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용히는 것이 원칙이다. 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다. 이때도 테스트는 일관성 있는 실행 결과를 보장해야 하고 테스트의 실행 순서가 결과에 영향을 미치지 않아야 한다. 다행히도 애플리케이션 컨텍스트는 초기화되고 나면 내부의 상태가 바뀌는 일은 거의 없다.

4.1 테스트를 위한 애플리케이션 컨텍스트 관리

스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 애노테이션 설정만으로 테스트에서 필요로 히는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.

스프링 테스트 컨텍스트 프레임워크

@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용히는 애노테이션이다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.

@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것이다.

테스트 메소드의 컨텍스트 공유

스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다. 일종의 DI라고 볼 수 있는데, 애플리케이션 오브젝트 사이의 관계를 관리하기 위한 DI와는 조금 성격이 다르다.

하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용할 수 있다.

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

스프링 테스트 컨텍스트 프레임워크의 기능은 하나의 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유 해주는 것이 전부가 아니다. 여러 개의 테스트 클래스가 있는데 모두 같은 설정 파일을 가진 애플리케이션 컨텍스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.

두 개의 테스트 클래스가 같은 설정 파일을 사용하는 경우에는 테스트 수행 중에 단 한 개의 애플리케이션 컨텍스트만 만들어진다. 두 테스트 클래스의 모든 메소드가 하나의 애플리케이션 컨텍스트 공유하게 되는 것이다.

수백 개의 테스트 클래스를 만들었는데 모두 같은 설정 파일을 사용한다고 해도 테스트 전체에 걸쳐 단 한 개의 애플리케이션 컨텍스트만 만들어져 사용된다. 이 덕분에 테스트 성능이 대폭 향상됨은 더 설명할 필요도 없을 것이다.

물론 테스트 클래스마다 다른 설정파일을 사용하도록 만들어도 되고, 몇 개의 테스트에서만 다른 설정파일을 사용할 수도 있다. 스프링은 설정파일의 종류만큼 애플리케이션 컨텍스트를 만들고, 같은 설정파일을 지정한 테스트에서는 이를 공유하게 해준다.

@Autowired

@Autowired는 스프링의 DI에 사용되는 특별한 애노테이션이다.

@Autowired가 붙은 인스턴스 변수가 있으면 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 일반적으로는 주입을 위해서는 생성자나 수정자 메소드 같은 메소드가 필요하지만, 이 경우에는 메소드가 없어도 주입이 가능하다. 또 별도의 DI 설정 없이 필드의 타입 정보를 이용해 빈을 자동으로 가져 올 수 있는데 이런 방법을 타입에 의한 자동와이어링이라고 한다.

스프링 애플리케이션 컨텍스트는 초기화 할 때 자기 자신도 빈으로 등록한다. 따라서 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하는 셈이고 DI도 가능하다.

@Autowired는 변수에 할당 가능한 타입을 가진 빈을 자동으로 찾는다. 이 때 구현 클래스 타입은 물론이고, 인터페이스 타입으로 변수를 선언해도 된다. 단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. 예를 들어 DataSource 타입의 빈이 두 개 이상 설정되어 있다면 문제가 될 수 있다.

@Autowired는 타입으로 가져올 빈 하나를 선택 할 수 없는 경우에는 변수의 이름과 같은 이름의 빈이 있는지 확인한다. 변수 이름으로도 빈을 찾을 수 없는 경우에는 예외가 발생한다.

테스트는 필요하다면 얼마든지 애플리케이션 클래스와 밀접한 관계를 맺고 있어도 상관없다. 개발자가 만드는 테스트는 코드 내부구조와 설정 등을 알고 있고 의도적으로 그 내용을 검증해야 할 필요가 있기 때문이다. 하지만 꼭 필요하지 않다면 테스트에서도 가능한 한 인터페이스를 사용해서 애플리케이션 코드와 느슨하게 연결해두는 편이좋다.

4.2 DI와 테스트

구현 클래스가 절대 바뀌지 않는다면, 굳이 인터페이스를 이용해서 DI를 받지 않아도 될까? 그렇지 않다. 구현 클래스가 절대로 바뀌지 않을거 같더라도, 가능하면 인터페이스를 이용해서 DI를 받는게 좋다. 그래야 하는 이유는 아래와 같다.

  • 첫째 : 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
  • 둘째 : 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
  • 셋째 : 테스트 때문이다. 단지 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다. 테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작 하도록 테스트 코드를 만들어야 한다. 그러기 위해서는 가능한 한 작은 단위의 대상에 국한해서 테스트해야 한다. 테스트 할 대상의 범위가 넓어지면 테스트를 작성하기가 어려워진다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.
테스트 코드에 의한 DI

테스트 코드 내에서 직접 DI 해도 된다.

스프링 테스트 컨텍스트 프레임워크를 적용했다면 애플리케이션 컨텍스트는 테스트 중에 딱 한 개만 만들어지고 모든 테스트에서 공유해서 사용한다. 따라서 애플리케이션 컨텍스트의 구성이나 상태를 테스트 내에서 변경하지 않는 것이 원칙이다. 그런데 애플리케이션 컨텍스트에 등록된 빈의 의존관계를 변경해야 할 때가 있다. 만약에 테스트에서 빈의 의존관계를 강제로 변경한다면, 나머지 모든 테스트를 수행하는 동안 변경된 애플리케이션 컨텍스트가 계속 사용될 것이다. 이는 별로 바람직하지 못하다.

@DirtiesContext 애노태이션은 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 테스트 컨텍스트는 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 테스트 메소드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다. 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 하기 위해서다.

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

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

테스트 코드에서 빈 오브젝트에 수동으로 DI 하는 방법은 장점보다 단점이 많다. 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트도 매번 새로 만들어야 하는 부담이 있다.

아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용해도 된다. 즉 두 가지 종류의 설정파일을 만들어서 하나에는 서버에서 운영용으로 사용할 DataSource를 빈으로 등록해두고, 다른 하나에는 테스트에 적합하게 준비된 DB를 사용히는 가벼운 DataSource가 빈으로 등록되게 만드는 것이다. 그리고 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해주면 된다.

컨테이너 없는 DI 테스트

DI를 테스트에 이용하는 방법은 아예 스프링 컨테이너를 사용하지 않고 테스트를 만드는 것이다. 구현 클래스 어디에도 스프링의 API를 직접 사용한다거나 애플리케이션 컨텍스트를 이용하는 코드가 존재하지 않는다면 굳이 스프링을 이용해서 테스트를 진행할 필요가 없다.

즉, 테스트하려는 클래스가 스프링 컨테이너에 의존하지 않는다면 스프링 컨테이너를 이용해서 IoC 방식으로 생성되고 DI 되도록 하는 대신, 테스트 코드에서 직접 오브젝트를 만들고 DI 해서 사용해도 된다.

DI는 객체지향 프로그래밍 스타일이다. 따라서 DI를 위해 컨테이너가 반드시 필요한 것은 아니다.DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아니다.

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

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

DI가 적용된 코드는 테스트에서도 다양한 방식으로 활용할 수 있을 만큼 유연하다. 어디에 DI를 적용할지 고민되는 경우, 효과적 인 테스트를 만들기 위해서는 어떤 필요가 있을지를 생각해보면 도움이 된다. 두 개의 모률이 강하게 결합되어 있어 DI가 불가능한 구조로 만든다면 테스트 할 때 불편해진다거나, 자동화 된 테스트가 아예 불기능하지 않을까 의심해보자. 일반적으로 테스트하기 좋은 코드가 좋은 코드일 가능성이 높다.

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

그렇다면 DI를 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야 할까? 세 가지 방법 모두 장단점이 있고 상황에 따라 유용하게 쓸 수 있다.

항상 스프링 컨테이너 없이 테스트 할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다. 테스트를 위해 필요한 오브젝트의 생성과 초기화가 단순하다면 이 방법을 가장 먼저 고려해야 한다.

여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우가 있다. 이 때는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다. 테스트에서 애플리케이션 컨텍스트를 사용하는 경우에는 테스트 전용 설정 파일을 따로 만들어 사용하는 편이 좋다. 보통 개발 환경과 테스트 환경, 운영 환경이 차이가 있기 때문에 각각 다른 설정 파일을 만들어 사용히는 경우가 일반적이다.

테스트 설정을 따로 만들었다고 하더라도 때로는 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우가 었다. 이 때는 컨텍스트에서 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하면 된다. 테스트 메소드나 클래스에 @DirtiesContext 애노테이션을 붙이는 것을 잊지 말자.

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

때로는 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서도 테스트를 작성해야 한다. 이런 테스트를 학습 테스트(learning test)라고 한다.

학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것이다. 따라서 테스트 이지만 프레임워크나 기능에 대한 검증이 목적이 아니다. 오히려 자신이 테스트를 만들려고 하는 기술이나 기능에 대해 얼마나 제대로 이해하고 있는지, 그 사용 방법을 바로 알고 있는지를 검증하려는 게 목적이다. 또, 테스트 코드를 작성해보면서 빠르고 정확하게 사용법을 익히는 것도 학습 테스트를 작성하는 하나의 목적이다.

5.1 학습 테스트의 장점

다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다

학습 테스트는 자동화된 테스트 코드로 만들어지기 때문에 다양한 조건에 따라 기능이 어떻게 동작하는지 빠르게 확인할 수 있다.

학습 테스트 코드를 개발 중에 참고 할 수 있다

학습 태스트는 다양한 기능과 조건에 대한 테스트 코드를 개별적으로 만들고 남겨둘 수 있다. 이렇게 테스트로 새로운 기술의 다양한 기능을 사용하는 코드를 만들어두면 실제 개발에서 샘플 코드로 참고할 수 있다. 아직 익숙하지 않은 기술을 사용해야 하는 개발자에게는 이렇게 미리 만들어진 다양한 기능에 대한 테스트 코드가 좋은 참고 자료가 된다.

프레임워크나 제품을 업그레이드 할 때 호환성 검증을 도와준다

학습 테스트에 애플리케이션에서 자주 사용히는 기능에 대한 테스트를 만들어 놓았다면 새로운 버전의 프레임워크나 제품을 학습 테스트에만 먼저 적용해본다. 기존에 사용했던 API나 기능에 변화가 있거나 업데이트된 제품에 버그가 있다면, 학습 테스트를 통해 미리 확인할 수가 있다. 버그가 있어서 테스트가 실패하면 업그레이드 일정을 늦추거나. API의 사용 방법에 변화가 발생한 경우라면 그에 맞춰서 애플리케이션 코드를 수정할 계획을 세울 수 있을 것이다.

테스트 작성의 좋은 훈련이 된다

학습 테스트를 테스트 작성의 훈련 기회로 삼는 것도 좋다. 또는 새로운 테스트 방법을 연구하는데도 도움이 된다. 기술에 따라서 테스트가 까다로운 것도 있는데 이럴 때 먼저 학습 테스트를 만들어보면서 간결한 테스트 작성 방법을 연구해보면 도움이 된다.

새로운 기술을 공부하는 과정이 즐거워진다

스프링 학습 테스트를 만들 때 참고할 수 있는 가장 좋은 소스는 바로 스프링 자신에 대한 테스트 코드다. 스프링은 꼼꼼하게 테스트를 만들어가며 개발해 온 프레임워크다. 거의 모든 기능에 대해 방대한 양의 테스트가 만들어져 있다. 스프링 배포판의 압축을 풀어보면 프레임워크 소스코드와 함께 테스트 코드도 발견할 수 있을 것이다. 스프링 테스트를 잘 살펴보면 레퍼런스 문서에서는 미처 설명되지 않았던 중요한 정보도 많이 얻을 수 있다. 또, 테스트 작성 방법에 대한 좋은 팁을 얻을 수 있을 것이다.

5.2 버그 테스트

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

버그 테스트는 일단 실패하도록 만들어야 한다. 버그가 원인이 되서 테스트가 실패하는 코드를 만드는 것이다. 그러고나서 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다. 테스트가 성공하면 버그는 해결된 것이다.

버그 테스트의 필요성과 장점을 생각해보자.

테스트의 완성도를 높여준다

기존 테스트에서는 미처 검증하지 못했던 부분이 있기 때문에 오류가 발생한 것이다. 이에 대해 테스트를 만들면 불충분했던 테스트를 보완해준다.

버그의 내용을 명확하게 분석하게 해준다

버그가 있을 때 그것을 테스트로 만들어서 실패하게 하려면 어떤 이유 때문에 문제가 생겼는지 명확히 알아야 한다. 따라서 버그를 좀 더 효과적으로 분석할 수 있다. 그 과정에서 그 버그로 인해 발생할 수 있는 다른 오류를 함께 발견할 수도 있다. 예를 들어 예외적인 상황이나 입력 값 때문에 발생히는 오류였다면, 테스트 코드를 만들면서 오류를 발생시키는 값의 범위가 어떤 것인지 분석해볼 기회가 주어진다. 테스트의 중요한 기법 중의 하나인 동등분할이나 경계값 분석을 적용해볼 수도 있다.

기술적인 문제를 해결하는 데 도움이 된다

때로는 버그가 있다는 건 알겠지만 그 원인이 무엇인지 정확하게 파악하기 힘들 때가 있다. 아무리 코드와 설정 등을 살펴봐도 별다른 문제가 없는 것 같이 느껴지거나 또는 기술적으로 다루기 힘든 버그를 발견하는 경우도 었다. 이럴 때는 동일한 문제가 발생하는 가장 단순한 코드와 그에 대한 버그 테스트를 만들어보면 도움이 된다.

  • 동등분할 : 갈은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법을 말한다. 어떤 작업의 결과의 종류가 true, false 또는 예외 발생 세 가지라면 각 결과를 내는 입력 값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는 것이 좋다.
  • 경계값 분석 : 에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다. 보통 숫자의 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트 해보면 도움이 될 때가 많다.

정리

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