Spring/토비의 스프링
[토비의 스프링 3.1 Vol1] "2장 테스트" 정리
y-seo
2023. 11. 14. 18:54
2.1 UserDaoTest 다시보기
2.1.1 테스트의 유용성
- 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지 확인하는 과정
- 테스트가 성공하면 모든 결함이 제거되었다는 의미
2.1.2 UserDaoTest의 특징
웹을 통한 DAO 테스트 방법의 문제점
- 웹 화면을 통해 값을 입력하고 기능을 수행하고 결과를 확인하는 방법이 가장 흔하다.
- 위 방법은 DAO에 대한 테스트로서는 단점이 너무 많다. DAO뿐만 아니라 서비스, 컨트롤러, 뷰 등 모든 레이어 기능을 만들고 나서 테스트가 가능하기 때문이다. 또한 테스트 중 문제가 생기더라도 범위가 광범위하여 원인을 알아내기 힘들다.
작은 단위의 테스트
- 테스트는 가능한 작은 단위로 쪼개어 집중해서 실행해야 한다. → “관심사의 분리”라는 원리가 여기에도 적용된다.
- 단위 테스트 = 작은 단위의 코드에 대해 테스트를 수행하는 것
- 여기서 단위는 충분히 하나의 관심에 집중하여 효율적으로 테스트할 만한 범위라고 정한다.
- DB 사용 여부에 따라 단위인지 아닌지 판단하기도 한다. → DB가 사용되면 단위 테스트가 아니라고도 할 수 있다는 뜻이다. 통제할 수 없는 외부의 리소스에 의존하면 단위 테스트가 아니라고 보는 것이다.
- 물론 통합적으로도 잘 수행하는지 확인하기 위해 긴 테스트가 필요하기도 하다. 이때도 단위 테스트를 미리 했다면 테스트 실패 원인을 찾기 더 쉬울 것이다.
자동수행 테스트 코드
- 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다. 자주 반복할 수 있기 때문이다.
- 클래스 안에 테스트 코드를 구성하는 것보다는 별도로 테스트용 클래스를 만드는 것이 낫다.
지속적인 개선과 점진적인 개발을 위한 테스트
- 작은 단계를 거치는 동안 테스트를 하나하나 다 수행하고 점차 코드를 변경해가야 한다. 그래야 전체적으로 코드를 개선하는 작업에 속도가 더 붙고 쉬워진다. 테스트 또한 점차 기능을 추가해나가며 점진적인 개발을 해나간다.
2.1.3 UserDaoTest의 문제점
- 수동 확인 작업의 번거로움
- 콘솔에 나온 값을 보고 등록과 조회가 성공적으로 진행되었는지 사람이 직접 확인해야 한다.
- 실행 작업의 번거로움
- main() 메소드를 여러 번 수행하고 기록하고 종합해야 하기 때문에 번거롭다.\
2.2 UserDaoTest 개선
2.2.1 테스트 검증의 자동화
- “add()에 전달한 User 오브젝트에 담긴 사용자 정보 = get()을 통해 다시 DB에서 가져온 User 오브젝트 정보” 인지 확인해야 한다.
- 테스트 에러 : 테스트가 진행되는 동안에 에러가 발생하여 실패하는 경우
- 콘솔에 에러 메세지와 긴 호출 스택 정보가 출력되어 쉽게 알 수 있다.
- 테스트 실패 : 테스트 작업 중에 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나오는 경우
- 별도의 확인 작업과 그 결과가 있어야 알 수 있다.
- if-else문을 통해 정보들이 같은지 확인하여 테스트가 성공 여부를 콘솔에 출력할 수 있다. → 결과 검증까지 테스트를 자동화 했다.
2.2.2 테스트의 효율적인 수행과 결과 관리
JUnit 테스트로 전환
- 자바로 단위 테스트를 만들 때 유용하게 사용할 수 있는 프레임워크
- 프레임워크의 기본 동작 원리가 IoC이기 때문에 main() 메소드와 오브젝트를 만들어 실행시키는 코드가 필요 없다.
- 라이브러리 추가가 필요하다.
테스트 메소드 전환
- 테스트가 main() 메소드로 만들어졌다는 것으 ㄴ제어권을 직접 갖는다는 의미이다.
- 따라서 main() 메소드에 있던 테스트 코드를 일반 메소드로 옮겨야 한다.
- JUnit 프레임워크가 요구하는 조건
- 메소드가 public으로 선언되어야 한다.
- 메소드에 @Test 어노테이션이 붙어야 한다.
검증 코드 전환
- if-else문을 asserThat() 이라는 static 메소드를 사용해 전환해줄 수 있다.
- assertThat() 메소드는 첫번째 파라미터의 값을 뒤에 나오는 matcher라고 불리는 조건으로 비교하여 일치하면 패스하고, 아니면 테스트가 실패하도록 만들어준다.
- is()는 matcher의 일종으로 equals()로 비교해주는 기능을 가진다.
- “테스트 성공” 이라는 메세지를 굳이 출력하지 않아도 테스트 결과를 JUnit이 다양하게 알려준다.
JUnit 테스트 실행
- 어디선가 한 번은 JUnit 프레임워크를 시작시켜 주어야 한다.
- 어디에든 main() 메소드를 하나 추가하고 그 안에 JUnitCore 클래스의 main 메소드를 호출하는 코드를 넣어주면 된다.
2.3 개발자를 위한 테스팅 프레임워크 JUnit
- JUnit 테스트의 장점
- 단순하여 빠르게 작성할 수 있다.
- 편리한 여러 가지 부가기능을 제공한다.
2.3.1 JUnit 테스트 실행 방법
IDE
- 테스트가 시작되면 JUnit 테스트 정보를 표시해주는 View가 나타나 테스트 진행 상황을 보여준다.
- JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수 있다.
- 테스트의 실행과 결과를 확인하는 방법이 간단하고 직관적이며 소스와 긴밀하게 연동된 결과를 볼 수 있기 때문에 IDE의 지원을 받는 것이 편리하다.
- Ex. 이클립스
빌드 툴
- 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용해 JUnit 테스트를 실행할 수 있다.
- 테스트 실행 결과는 옵션에 따라 HTML이나 텍스트 파일의 형태로 보기 좋게 만들어진다.
- 여러 개발자가 만든 코드를 모두 통합하여 테스트를 수행해야 할 때는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다. 이때 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 그 결과를 메일 등으로 통보 받는 방법을 사용하면 된다.
2.3.2 테스트 결과의 일관성
- 테스트가 외부 상태에 따라 성공 여부가 갈린다. → 이러면 안되고 테스트 결과는 항상 똑같아야 한다.
- 이를 위해 테스트를 마치고 나면 테스트를 수행하기 이전 상태로 만들어주어야 한다.
deleteAll()의 getCount() 추가
- UserDao에 새로운 기능을 추가하여 일관성 있는 결과를 보장하는 테스트로 만든다.
- deleteAll()
- USER 테이블의 모든 레코드를 삭제한다.
- add() 메소드와 비슷한 구조이다.
- getCount()
- USER 테이블의 모든 레코드 개수를 돌려준다.
- get() 메소드와 비슷한 구조이다.
deleteAll()과 getcount()의 테스트
- 추가된 기능에 대한 테스트도 만들어야 한다.
- 새롭게 만드는 것보다는 addAndGet() 테스트를 확장하는 식으로 테스트 한다.
- 무작정 새로운 메소드를 넣는 것은 바람직하지 못하므로 deleteAll()을 검증할 만한 getCount() 메소드를 같이 추가하여 검증 작업을 하나 더 늘린다.
동일한 결과를 보장하는 테스트
- 단위 테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.
- 테스트를 실행하기 전에 문제가 되지 않는 상태를 만들어주는 것이 낫다.
2.3.3 포괄적인 테스트
getCount() 테스트
- JUnit은 하나의 클래스 안에 여러 개의 테스트 메소드가 들어가는 것을 허용한다.
- @Test가 붙어 있고
- public 접근자가 있고
- return 값이 void 형이고
- 파라미터가 없다면
- JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다. 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야 한다.
addAndGet() 테스트 보완
- get() 메소드가 주어진 id에 해당하는 사용자를 가져온 것이 맞는지 보완해야 한다.
get() 예외조건에 대한 테스트
- get() 메소드에 전달된 id값에 해당하는 사용자 정보가 없다면 어떻게 해야 할지 보완해야 한다.
- null을 리턴할 수도
- 예외를 던질 수도
- 예외를 던지는 방법을 쓴다면 예외 클래스가 하나 더 필요하다. → EmptyResultDataAccessException
- 일반적으로는 테스트 중에 예외가 던져지면 테스트 메소드의 실행이 중단되고 테스트는 실패한다. (테스트 에러)
- 반대로 테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한다. 예외가 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패한다고 판단해야 한다. → 예외 발생 여부를 직접 확인해야 한다. 이런 경우에 JUnit이 예외조건 테스트를 위한 별도의 방법을 제공한다.
- 모든 데이터를 지우고 존재하지 않는 id로 get()을 호출한다. 이때 예외가 던져지면 성공이다.
- @Test(expected=예외명) 으로 테스트 중에 발생할 것으로 기대하는 예외 클래스를 지정할 수 있다.
- 예외가 반드시 발생해야 하는 경우를 테스트할 때 사용한다.
테스트를 성공시키기 위한 코드의 수정
- 생략
포괄적인 테스트
- DAO의 메소드에 대한 포괄적인 테스트를 만들어두는 편이 훨씬 안전하고 유용하다.
- 여러 시나리오에 따른 테스트가 성공되어야 한다. 성공할만한 테스트만 하면 안된다.
- 부정적인 케이스를 먼저 만드는 습관을 들이는 것이 좋다.
2.3.4 테스트가 이끄는 개발
기능설계를 위한 테스트
- 추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 코드 수정보다 테스트 케이스 작성이 우선되었다.
- 테스트에는 다음 3가지 단계가 포함된다.
- 어떤 조건을 가지고
- 무엇을 할 때
- 어떤 결과가 나온다
- 마치 코드로 된 설계문서를 만든 것과 같다.
테스트 주도 개발
- TDD (Test Driven Development, 테스트 주도 개발)
- 만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고 테스트를 성공하게 해주는 코드를 작성하는 방식
- = Test First Development, 테스트 우선 개발
- 원칙 : “실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다”
- 코드에 대한 피드백을 빠르게 받을 수 있다.
- 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 짧게 가지는 것이 좋다.
- 자연스럽게 단위 테스트를 만들 수 있다.
- 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다.
2.3.5 테스트 코드 개선
- 테스트 코드도 리팩토링을 한다.
- JUnit 프레임워크는 반복되는 작업을 별도의 메소드에 넣게 해주고 이를 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜 준다.
@Before
- JUnit 프레임워크가 테스트 메소드를 실행하는 과정
- 테스트 클래스에서 @Test가 붙은 public이고 void형이고 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
- @Before가 붙은 메소드가 있으면 실행한다.
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
- @After가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 모든 테스트의 결과를 종합해서 돌려준다.
- 테스트 메소드에서 @Before이나 @After가 붙은 메소드를 자동으로 실행하지 않기 때문에 정보나 오브젝트를 인스턴스 변수로 선언해야 한다.
- 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다. 한 번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다. JUnit이 테스트가 각각 독립적으로 실행됨을 보장해주기 위해서이다.
픽스처
- 테스트를 수행하는 데에 필요한 정보나 오브젝트
- 일반적으로 @Before 메소드를 이용해 생성해두면 편리하다.
2.4 스프링 테스트 적용
- 애플리케이션 컨텍스트가 생성되면 모든 싱글톤 빈 오브젝트를 초기화한다.
- 테스트는 가능한 독립적으로 매번 새로운 오브젝트를 만들어 사용하는 것이 원칙이다.
- 하지만 시간과 자원이 많이 소요되는 경우에는 공유 오브젝트를 만들기도 한다.
- 애플리케이션 컨텍스트는 초기화되고 나면 내부의 상태가 바뀌는 일은 거의 없다. 빈은 싱글톤으로 만들었기 때문에 상태를 갖지 않는다. 따라서 애플리케이션 컨텍스트는 한 번만 만들고 여러 테스트가 공유해서 사용해도 된다.
- JUnit은 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다. 이 메소드에서 애플리케이션 컨텍스트를 만들어 스태틱 변수에 저장해두고 테스트 메소드에서 사용하게 할 수 있다.
- 하지만 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 더 편리하다.
2.4.1 테스트를 위한 애플리케이션 컨텍스트 관리
스프링 테스트 컨텍스트 프레임워크 적용
- @RunWith, @ContextConfiguration, @Autowired 어노테이션을 붙여준다.
- @RunWith : JUnit 프레임워크의 테스트 실행 방법을 활장할 때 사용
- @ContextConfiguration : 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것
테스트 메소드의 컨텍스트 공유
- JUnit 확장기능은 테스트가 실해오디기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용하여 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해준다. = 일종의 DI
- 하나의 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유하여 사용할 수 있다.
테스트 클래스의 컨텍스트 공유
- 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.
- 스프링은 설정의 종류만큼 애플리케이션 컨텍스트를 만들고 같은 설정파일을 지정한 테스트에서는 이를 공유하게 해준다. → @ContextConfiguration 에서 확인 가능
@Autowired
- 스프링의 DI에 사용되는 어노테이션
- @Autowired 가 붙은 인스턴스 변수가 있다면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다.
- 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있다 = 타입에 의한 자동와이어링
- 스프링 애플리케이션 컨텍스트는 초기화 할 때 자기 자신도 빈으로 등록한다.
- @Autowired는 타입으로 가져올 빈 하나를 선택할 수 없는 경우에는 변수의 이름과 같은 이름의 빈이 있는지 확인하여 같은 이름의 빈을 가져온다.
2.4.2 DI와 테스트
- 인터페이스를 사용하고 DI를 통해 주입해주는 방식을 사용해야 한다.
- SW 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
- 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
- 테스트 단위를 줄이기 위해서이다.
테스트 코드에 의한 DI
- XML 설정 파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있다.
테스트를 위한 별도의 DI 설정
- DI의 장점을 살려 DAO가 테스트에서만 다른 DataSource를 사용하게 할 수 있는 방법은?
- DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법 가능
- 두 가지 종류의 설정파일을 만들어 하난느 서버에서 운영용으로 사용할 DataSource를 빈으로 등록해두고, 다른 하나에는 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource가 빈으로 등록되게 만드는 것이다.
- @Contextconfiguration 의 locations 엘리먼트 값을 새로 만든 테스트용 설정파일로 변경해준다.
컨테이너 없는 DI 테스트
- @Autowired. @RunWith가 없다.
- @Before 메소드에서 직접 UserDao의 오브젝트를 생성하고 테스트용 DataSource 오브젝트를 만들어 직접 DI 해주었다.
- 번거로움은 있지만 코드는 더 단순하고 이해하기 편해졌고 테스트 시간도 절약되었다.
침투적 기술과 비침투적 기술
- 침투적(invasive) 기술은 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API가 등장하거나 특정 인터페이스나 클래스를 사용하도록 강제하는 기술을 말한다. → 종속된다.
- 비침투적(noninvasive) 기술은 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다. → 종속적이지 않다, 스프링이 이렇기에 스프링 컨테이너 없는 DI 테스트가 가능하다.
DI를 이용한 테스트 방법 선택
- 스프링 컨테이너 없이 테스트할 수 있는 방법을 우선적으로 고려하자.
- 수행 속도가 빠르고 간결하다.
- 테스트에 필요한 오브젝트의 생성과 초기화가 단순하다면 가장 먼저 고려할 방법이다.
- 스프링 설정을 이용한 DI 방식의 테스트를 이용하자.
- 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우
- 각각 다른 설정 파일을 만들어 사용하는 경우가 일반적이다
- 컨텍스트 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하자.
- 예외적인 의존관계를 강제로 구성해야 할 때
- @DirtiesContext 추가 필요
2.5 학습 테스트로 배우는 스프링
- 학습 테스트
- 자신이 만들지 않는 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서 테스틀르 작성해야 하는 것
- 목적 : 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히기 위함
2.5.1 학습 테스트의 장점
- 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
- 학습 테스트 코드를 개발 중에 참고할 수 있다. → 기록해둔/된 것이 있기 때문이다.
- 프레임워크나 제품을 업그레이드 할 때 호환성 검증을 도와준다. → 학습 테스트를 미리 실행하여 어떤 변화가 있는지 확인할 수 있기 때문이다.
- 테스트 작성에 대한 좋은 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다.
- 참고할 만한 가장 좋은 소스는 스프링에 대한 테스트 코드이다.
2.5.2 학습 테스트 예제
JUnit 테스트 오브젝트 테스트
- 새로운 테스트 클래스 생성한다.
- 적당한 이름으로 세 개의 테스트 메소드 추가한다.
- 테스트 클래스 자신의 타입으로 static 변수를 하나 선언한다.
- 매 테스트 메소드에서 현재 static 변수에 담긴 오브젝트와 자신을 비교하여 같지 않다는 사실을 확인한다.
- 현재 오브젝트를 그 static 변수에 저장한다.
- 이렇게 하면 직전의 오브젝트와만 비교할 수 있다.
- static 변수로 테스트 오브젝트를 저장할 수 있는 collection을 만든다.
- 테스트마다 현재 테스트 오브젝트가 collection에 이미 등록되어 있는지 확인하고 없으면 추가한다.
- 위 과정을 반복한다.
- 이렇게 하면 테스트가 어떤 순서로 실행되는지에 상관없이 오브젝트 중복 여부를 확인할 수 있다.
스프링 테스트 컨텍스트 테스트
- 설정 파일을 생성한다.
- @RunWith, @ContextConfiguration 을 추가한다.
- 방금 만든 설정 파일을 사용하는 테스트 컨텍스트를 적용한다.
- @Autowired로 주입된 context 변수가 같은 오브젝트인지 확인하는 코드를 추가한다.
- context를 저장해둘 static 변수인 contextObject가 null인지 확인한다
- null이면 첫번째 테스트이므로 통과한다.
- 다음부터는 null이 아니므로 현재의 context가 같은지 비교할 수 있다.
- 이를 위해 assertThat() 혹은 assertTrue() 방식을 사용할 수 있다.
- assertThat()을 이용해 matcher와 비교할 대상인 첫번째 파라미터에 Boolean 타입의 결과가 나오는 조건문을 넣는다. 이후 그 결과를 is() matcher을 사용하여 true와 비교한다.
- 조건문을 받아 그 결과가 true인지 false인지 확인하도록 만들어진 assertTure() 메소드를 assertThat() 대신 사용한다.
- matcher의 조합을 이용하여 assertThat() 메소드를 사용한다. 두 개의 matcher을 OR 조건으로 비교하고 하나라도 true가 나오면 성공이 된다.
2.5.3 버그 테스트
- 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트
- 버그가 원인이 되어 테스트가 실패하도록 만들어야 한다. 이후 테스트가 성공할 수 있도록 코드를 변경한다.
- 버그 테스트의 필요성과 장점
- 테스트의 완성도를 높여준다.
- 버그의 내용을 며오학하게 분석하게 해준다.
- 기술적인 문제를 해결하는 데에 도움을 준다.
동등 분할 (equivalence partitioning)
- 같은 결과를 내는 값의 범위를 구분하여 각 대표 값으로 테스트를 하는 방법
- Ex. true, false, 예외 일 경우 이 3가지 모두에 대한 테스트를 한다.
경계값 분석 (boundary value analysis)
- 에러가 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용하여 경계의 근처에 있는 값을 이용해 테스트하는 방법
- 보통 숫자의 입력 값인 경우 0, 주변 값, 최대값, 최소값 등으로 테스트 해본다.