[스프링부트 (8)] SpringBoot Test(1) - Junit 설정 및 실행
기존에는 인프런 강의를 들으며 , 강사님께서 작성하시던 테스트코드 따라치기 경험밖에 없었다.
어쨋거나 user case를 테스트 해보기 위해 , 매번 서버를 실행하고, test를 위해 직접 db에서 기존 test를 위해 넣은 데이터를 삭제하거나, 현재 테스트를 위해서 직접 데이터를 넣는 과정이 반복되며 불편함을 느끼게 되었다.
unit test.. 이것을 도입은 해봐야할텐데
unit test가 정확히 뭐지?
Spring DATA JPA를 사용하는 중, unit test의 도입이 가능할까? 결국 서버를 run시켜야하는 거 아닐까?
Mock이 뭐지 ?
순수 JAVA 테스트를 할 수 있도록 할 줄 알아야 한다고 했었는데, 이건 또 어떤 의미일까? 왜 그렇게 해야할까 ?
Authorization이 들어가게 되면, 테스트 과정이 번거러워지는 것 아닐까?
API는 어떻게 테스트하지 ?
테스트
테스트.. 왜 필요할까??
- 내가 예상했던 대로 코드가 정확히 동작하는지를 확인해, 만든 코드를 확신할 수 있게 해 주는 작업이다.
“작은 테스트”의 도입 필요성
웹 앱이다 보니, 하나를 테스트하고 싶을 뿐인데, 모든 계층의 기능을 다 만들고 나서야 테스트가 가능하다는 문제점이 나타나고 있다.
또한 에러 발생시, 어디서 에러가 발생했었는지 에러코드를 통해 봐야하는 수고.. ( 내가 테스트하려고 한 곳에서 발생한 에러가 아닌 경우도 존재 )
- 하나의 테스트를 수행하는데 참여하는 class와 코드가 너무 많기 때문임.
- 작은 단위의 테스트가 필요해...
- 테스트 하고자 하는 대상에만 집중해 테스트 하는 것이 바람직하다 .
- ex) UserDaoTest 는, UserDao 라는 작은 단위의 기능만을 테스트하기 위함. “다른 계층은 참여하지 않음”
- 많은 것을 몰아서 테스트하면, 오류가 발생해도, 정확한 원인을 찾는데에 또 시간이 든다.
- 테스트에서도 "관심사의 분리" 가 필요해
- 테스트 하고자 하는 대상에만 집중해 테스트 하는 것이 바람직하다 .
DB가 사용되는 테스트는 단위테스트가 아닌가?
그런 것은 아니다.
하지만 반드시 고려해야할 것이 있다.
- 사용할 DB의 상태를 테스트가 관장하고 있어야 한다.
- 즉, 각 테스트를 진행하기 전 상태와 , 각 테스트 진행 후의 상태가 같아야 한다 ( 매 번, 테이블 내용을 비우고 테스트한다던가 )
- 만약. DB의 상태가 “매번 달라지고”, 테스트를 위해 “DB를 특정 상태로 만들어줄 수 없다면”, 단위테스트로서의 가치가 없어진다.
통제할 수 없는 외부 리소스에 의존하는 테스트는 단위테스트가 아니다.
라고 보기도 한다.
단위 테스트만 필요한가?
사용자를 등록 → 로그인 → 로그인 후 기능 사용 → 로그아웃 까지의 전 과정을 “하나로 묶어 테스트” 할 필요도 있다.
이렇게 “길고, 많은 단위들이 참여하는 테스트” 도 필요하다.
단위 테스트 도입의 장점.
- 개발자 스스로 빨리 확인받을 수 있음.
- 만약, 테스트가 고객이 테스트를 하는 시점이면 ? → 코드를 작성한지 오랜 시간 후임... 오류발생했다면? 오래전에 작성한 코드를 다시보고 원인을 찾아야함.
- 단위테스트를 한다면? 방금 작성한 코드에서 원인을 찾아야함 . 훨씬 쉽겠지...
- 테스트할 데이터를 코드로 제공하고, 테스트 작업 실행 역시 자동으로 실행할 수 있다.
- 자동 수행되는 테스트 → 자주 반복 가능하다.
- 언제든 실행가능한 테스트코드.
- 기능을 추가할 때마다, 작성했던 모든 테스트를 실행한다면, 추가한 기능으로인해, 타기능에 문제가 발생하진 않았는지 빠르게 확인 가능..
지속적 개선과 점진적 개발 속에서, 개발자 마음의 안정
매 번 테스트를 통해, 오류를 바로 확인 가능.
- 코드를 개선해 나가는 과정에서 자동화된 테스트를 통해 테스트를 거치고 이를 거쳤기 때문에, 확신을 갖고 코드를 변경할 수 있다.
- 기능을 추가하면서, 그에 대한 테스트를 추가하는 식의 점진적 개발이 가능하다.
- 새로운 기능에 대한 테스트
- 기존의 기능이 여전히 정상동작하는지 테스트 가능.
테스트는
테스트는 자동으로 수행되도록, 코드로 만들어지는 것이 중요하다.
테스트는 빠르게 실행가능해야한다. 따라서 작은 단위로 나누어질 수록 좋다.
- 테스트하고자 하는 대상에만 집중할 수 있는 테스트가 좋다 . ( 여러 단위가 참여할 수록, 원인을 찾기 어렵다 )
- 테스트는 기대한 결과에 대한 확인을 해 주는, 코드로된 자동화된 테스트가 좋다.
- 별도 테스트용 클래스를 만들어, 테스트 코드를 넣어야 한다.
중요
- 빠르게 실행할 수 있는 테스트
- 스스로 테스트 수행과, 기대하는 결과에 대한 확인까지 해주는 코드로된 , 자동화된 테스트
JUnit 도입
프레임워크의 기본 동작 원리 : 제어의 역전 (Ioc)
- “ 개발자가 만든 클래스 ”에 대한 제어권한을 넘겨 받아 주도적으로 app흐름을 제어
- 개발자가 만든 class의 객체를 생성,실행하는 일은 framework에 의해 진행.
- 따라서 main() 메소드도 필요없다.
- 테스트가 main() 메소드로 만들어졌다는 것 == 제어권을 직접 갖는다는 의미.
- JUnit을 통해, 테스트 코드를 "일반 메소드"로 옮겨보자
테스트 메소드로 전환
- Junit 프레임워크가 요구하는 조건
- 메소드가 public으로 선언될 것.
- 메소드에는 @Test 어노테이션이 붙어야 한다.
- 테스트 의도를 알 수 있는 메소드 이름 을 붙이자
- if,else가 아닌 검증 코드 → JUnit이 제공하는 방법(ex_assertThat) 으로 전환해보자.
- assertThat() 메소드는 첫 번째 파라미터의 값을, 뒤에 나오는 matcher 라는 조건으로 비교해, 일치하면 Pass하고 아니면 fail 하도록 만들어준다.
assertThat(user2.getPassword(), is(user.getPassword());
- Junit에서는 테스트 성공으로 인해 “ 테스트 성공” 이라는 메시지를 굳이 출력 할 필요 없다.
Junit 테스트 실행?
Junit 프레임워크 역시, 자바코드로 만들어진 프로그램이다. 어디선가 한 번은 Junit 프레임워크를 시작시켜 줘야 한다.
어디엔가 main() 메소드를 하나 추가하고, 그 안에 JUnitCore 클래스의 main 메소드를 호출해주는 코드를 넣어주면 될 것이다.
스프링 부트의 경우
@SpringBootTest
class FireboardApplicationTests {
assertThat 이용 검증 실패시 Exception
- AssertionError 를 던진다.
테스트 결과의 일관성 , 동일 결과 보장 테스트
테스트가 외부상태에 따라 성공하기도, 실패하기도 한다면???
—> BAD TEST!!!!!💥💥
- 코드에 변경 사항이 없다면, 테스트는 항상! 동일한 결과를 내야함.( 매 번, 실행 할 때 마다, 동일 테스트 결과를 얻을 수 있어야 한다 )
- “동일한 결과를 보장” 하는 테스트 여야 한다.
이전테스트로 생긴 DB의 데이터에 영향을 받으면 안되겠지
- 테스트를 마치고 나면, 테스트가 등록한 데이터를 삭제 하거나, 테스트 수행 전 마다, DB를 일정 상태로 만들어 주는 것이 필요하다.
- 테스트 수행 전 상태로 만들어 주는 것이 필요하다
- 이왕이면 “테스트 전” 에 테스트 실행이 문제가 되지 않는 상태를 만들어주는 것이 좋다.
- 이를 위해, 예를들어 Repository class에서는 deleteAll() 메소드를 정의해두는게 필요하겠지.
두 개 이상의 메소드는 필요한 테스트인 경우
예를들어 delteAll(), getCount() 메소드를 Repository class에 추가했어.
이게 잘 동작하는지 확인하려면 user를 추가하고, deleteAll을 하고 getCount를 해 볼거야.
근데 만약 내 getCount가 맨날 0을 주는 버그를 가지고 있을 수 있기 때문에 → getCount 역시 테스트 해보기 위해
하나를 add→getCount 로 1이 나오는지 테스트 해보고 → deleteAll을 해보고 getCount 로 0이 나오는지 테스트 해 볼 수 있겠다.
포괄적인 테스트
- 성의 없이 테스트를 만드는 바람에 문제가 있는 코드임에도 테스트가 성공하게 만드는 것은 매우매우 위험하다.
- 특히 한가지 결과만 검증하고 마는 것은 상당히 위험하다.
따라서, JUnit을 사용하여, 하나의 클래스 안에 여러개의 테스트 메소드를 넣어보자.
- @Test 가 붙어있고
- public접근자가 있으며
- 리턴 값이 void
- 파라미터가 없는 테스트메소드이면 된다.
JUnit 사용시, 클래스내 테스트의 실행순서는 알 수 없다.
JUnit은 특정 테스트 메소드의 실행 순서를 보장해주지 않는다.
- 테스트 “ 실행 순서에 영향을 받는 테스트” 는 잘못 만든것
- 모든 테스트는, 실행순서 상관없이, 독립적으로 항상 동일 결과 낼 수 있어야 함
예외조건 테스트
- 예외를 던져야 하는 경우에 대한 테스트를 진행하는 것도 필요하다.
- 예를들어 , Data binder에 대한 테스트를 하고 싶다. 숫자가 아닌 문자열을 Integer로 바인딩하려고 하는 것이기에 type mismatch관련 오류가 떠야 한다.
- 즉, 이 테스트에서는, 이 관련 오류가 뜨는 것이 테스트의 성공을 의미한다.
- JUnit에서는 이런식으로 " 예외가 반드시 발생해야하는 경우를 테스트"하기 위한 것을 지원
- @Test(expected = 예외.class ) 즉, 테스트 중 발생할 것으로 기대되는 예외 클래스를 지정
- 또는, 요즘 나는 아래의 것을 자주 이용했었다.
- @Test
static <T extends Throwable>T assertThrows(Class<T> expectedType, Executable executable)
테스트를 성공시키기 위해! “코드를! 수정”하자
즉, test를 성공시키기 위해 테스트를 수정하는 것이아니라
test를 성공시키기 위해, 테스트 대상인 코드를 수정하자는 것이다.
- 테스트코드는, 우리가 원하는 기능이 제대로 동작하는지에 대한 테스트다. 즉 테스트대로 수행이되어야 한다. 테스트가 실패하고 있다면, 원인을 파악하여 코드를 수정하여야 한다.
항상 네거티브 테스트를 먼저 만들어라
테스트 작성시, 부정적 케이스를 먼저 만드는 습관을 들이는게 좋다
- 예외 상황은 모두 피하고, 정상 케이스만 테스트하는 것은 해서는 안 된다 !!!!
- 다양한 시나리오를 통해, 다양한 경우에 대한 전문적 테스트 수행 필요가 있다.
ex) 존재 않는 id가 주어졌을 때는 어떻게 반응할지를 먼저 결정 → 이를 확인 할 수 있는 테스트를 먼저 만든다면, 예외적 상황을 빠트리지 않는 꼼꼼한 개발이 가능하다
테스트가 이끄는 개발 ? TDD (테스트주도개발), TFD(테스트우선개발)
- Test할 대상인 코드도 안 만들어 놓고 ( UserService의 join기능도 안 만들어놓고 ), 테스트 코드부터 만드는 ( join기능을 테스트하는 코드를 작성 ) 것
- 이런 순서를 위한 개발 전략
추가하고 싶은 기능을, 코드로 표현할 수가 있어진다.
이 테스트에는
추가하고 싶은 기능에 대한 🚀**"조건(given)","행위(when)","결과에 대한 내용(then)"**🚀 ****을 잘 표현해야 한다.
- 마치, 기능 정의서 처럼 보인다.
- 이런식으로 추가하고싶은 기능을, 테스트 코드로 표현! → 설계문서처럼!
- 그러고나서, 실제 기능을 만들면 → 테스트 코드로 바로 동작 검증가능
- 실제, [ 설계한대로 ] 코드가 동작하는지를 검증할 수 있는 것 !!!!
- 즉, 테스트가 성공하면
- 코드구현
- 테스트
- 만들고자 하는 기능의 내용을 담음 + 만들어진 코드 검증도 가능
TDD 왜 필요 ?
코드를 만들고 나면 “시간이 지날 수록”, 테스트 만들기 귀찮아져..
작성한 코드가 많아질수록, 무엇을 테스트해야하는지도 막막해진다.
테스트의 우선순위가 밀려난다.
- TDD는 아예, 테스트를 먼저 만들고!!!, 그 테스트가 성공하도록하는 코드만 만드는 식 !!
TDD를 하면
- 테스트를 빼먹지 않고, 꼼꼼하게 만들 수 있다.
- 테스트 작성시간과, app코드 작성하여, 테스트를 실행하는 시간의 간격이 거의 0 가깝다
- 테스트를 만들어뒀기에, 기능 구현 코드 작성 후, 바로 테스트를 수행 가능하니까
- 오류는 빨리 발견할 수록 좋다.but, 테스트 덕분에 오류를 빨리 잡아낼 수 있어, 오히려 전체적 개발 속도가 빨라질 수 있다.
- 테스트 코드작성으로 오히려 개발속도가 지연되지 않을까??? 라는 걱정이 들 수도 있다.
JUnit
@Before
- 테스트 실행 때 마다, 반복되는 준비작업이 있을 수 있다.
- 이를 별도의 메소드에 넣고
- 매 test method 실행 전에, 해당 메소드를 먼저 실행 시켜 줄 수 있다.
- 중복 코드를 제거 가능
JUnit framework
프레임워크라는 것
- 🚀프레임워크는 [ 스스로 제어권을 갖고, 주도적으로 동작 ] 한다.
이런것들을 떠올려보자
- 할리우드 원칙
- Template method
- 반면 개발자 코드는, [ 프레임워크에 의해 수동적으로 실행 ] 된다.
- 따라서, 실행흐름이 잘 보이지 않을 것
이 실행흐름을 간단히 알아보자
JUnit이 하나의 test class를 가져와 테스트를 수행하는 방식 ( 간단하게 )
- Test class에서 [ @Test 가 붙은 public인 , void형, parameter가 없는 ] 테스트 메소드를 모두 찾는다.
- Test class object를 생성
- @Before 메소드가 있으면 실행
- @Test 메소드 [ 하나 ]를 호출 → 결과를 저장
- @After 메소드가 있으면 실행
- 모든 테스트 메소드에 대해 2~5를 실행
- 모든 테스트 결과를 종합 → 돌려준다.
매 번 테스트 클래스 객체를 생성 . WHY??!
- 각 Test method를 실행 할 때 마다!! 테스트 클래스의 객체를 새로 생성한다.
- 생성된 객체는, 하나의 테스트 메소드를 호출해주고는 버려진다.
- 왜?? 매 번 객체를 생성????
🚀각 TEST가, 서로 영향을 주지 않고 ,독립적으로 실행됨을 확실히 보장🚀해 주기 위해!!
인스턴스 변수의 필요성
- @Before, @After method를 통해 → 테스트 클래스내 테스트 메소드들의 공통 작업을 자동 수행 가능.
- But, Test method에서 직접 @After @Before 를 호출하는게 아니다보니
- 서로 주고받을 정보는 “인스턴스 변수”를 통해야함 (
하나의 테스트 클래스?
- 일부 테스트 메소드들에서만 공통적으로 사용되는 코드가 있다면??
- @Before 보다는, 일반적인 extract method를 통한 “메소드 분리” 를 하거나
- 공통 특징을 지닌 테스트 메소드들을 모아, 별도의 테스트 클래스를 생성한다.
Fixture
테스트를 수행하는데 필요한 정보 or 객체
- 일반적으로, 여러 테스트에서 반복적으로 사용되는 객체
- @Before 메소드를 이용해 생성해 두면 편리하다
예를들어, UserServiceTest 라면, UserRepository가 대표적인 fixture다
❓❓❓ 인스턴스 변수를 두는 것과의 차이점이 뭐지 ?
- 인스턴스 변수로 “선언” 해 두고
- @Before method에서 객체 생성을 진행
또는.. 어차피 “ 매 번 새로운 테스트 객체” 가 생성되니, 인스턴스 변수의 선언과 동시에 초기화를 해 두어도 상관없음
- but, fixture 생성 로직을 모아두는 것이 좋을테니 @Before 메소드를 이용
Spring Test?
Unit Test에서 중요한 것
테스트는 [ 모든 단위테스트에 대해, 각 테스트의 일관성 있는 실행 결과를 보장 ] 해야 하고
단위 테스트들의 [ 실행 순서가 결과에 영향을 미치면 안된다 ]
Test 결과의 일관성
예를들어 어떤 test에서는, DB에 데이터를 삽입하는 과정이 들어있다. 그렇다면 이 테스트를 진행하고 나면, DB에는 테스트 과정에서 삽입한 데이터들이 들어있게 된다. 따라서 이 데이터를 모두 삭제해 줘야 하는 번거로움이 생긴다.
- 같은 테스트 메소드를 다시 실행한다면, 중복된 pk 를 가진 데이터로 인해 에러가 발생할 것이다.
- 다른 테스트 메소드에서 DB에 삽입해 놓은 데이터에 의한 영향을 받을 수도 있다.
- 즉, 각 단위 테스트를 실행하는 순서를 바꿈에 따라, *동일한 결과가 나오지 않게 될 수*도 있다.
즉, [ Test가 외부 상태에 따라 성공 or 실패하기도 ] 하는 상황
→ 💥💥 좋은 테스트가 X
- 코드에 변경사항이 없다면, 테스트는 항상 동일 결과를 내야 한다.
동일 결과 보장을 위해 Test method마다 Roll-back을 해 주면 되지 않나요?
A 테스트 실행을 마치면서, 테스트가 변경하거나 추가한 데이터를 모두 원래 상태로 만들어주면 되지 않을까??
이거로는 해결하지 못하는 문제가 존재한다.
- A 테스트 실행 전, 다른 이유로 테이블에 들어가있던 데이터로 인해 A테스트나 B테스트 등의 테스드가 실패할 수 있다.
테스트 “후”보다는
테스트 “전”에 테스트 실행에 문제가 되지 않는 상태를 만들어주는게 더 나을 것.
JUnit동작 방식 그리고 Spring application context
- 생각해보자.. 앞서, JUnit은 하나의 테스트 메소드를 실행 시킬 때 마다, 테스트 클래스 객체를 새로 생성해야 했다.
- Spring 의 “제어의 역전(IoC)” 개념을 떠올려 보자
- Ioc
어떤 객체를 사용하려는 경우, 개발자가 직접 코드에 어떤 상황에서 “어떤” 객체를 “언제,어떻게” 생성 하고, 어떤 객체의 메소드를 호출 하는 등의 흐름을 “사용하는 측에서 직접 제어”하던 것 ( 즉 , 객체의 “생성”, “연관관계” “구체 클래스 결정” 과 같은 것을, 사용자가 아닌 제3 자가 제어하는 것 )- Inversion Of ControlSpring이라는 프레임워크가 이러한 흐름을 제어하는 것.- 이 중심에는 Application Context라는 팩토리가 존재했다.
- Spring Bean : Spring Container가 “생성,관계설정,사용” 등을 제어해 주는 “제어의 역전이 적용된 객체” 였다.
- Spring에서 “빈의 생성,관계설정” 같은 “제어를 담당하는 IoC 객체”를 “ 빈 팩토리” 라고 부른다.
- 보통 “빈 팩토리” 보다는 “application context” 를 주로 사용한다.
이 아닌
- Spring Test를 하기 위해서는, Application Context를 생성해야 한다.
- 모든 테스트 메소드는, Application Context가 필요한데
- @Before method에서 application context를 생성하도록 한다면? → test method를 실행할 때 마다 생성됨. 테스트 메소드가 3개라면, application context도 3번 생성된다.
- ApplicationContext 생성 시, 모든 singleton bean 객체를 초기화한다.
- 어떤 bean 은 자체적 초기화를 필요로 할 것. ( 많은 리소스를 할당할 수도, 독립적 스레드를 띄울 수도.. )
- 초기화작업에 시간도 많이 들고
- 테스트 마칠 때 마다, 리소스를 깔끔하게 정리하지 못할 경우 문제 발생 가능.
Test 전체가 공유하는 객체 생성
- test는 [ 가능한 독립적으로, 매 번, 객체를 생성해 사용 ] 하는 것이 원칙이나
- application context처럼, 많은 시간,자원이 소모되는 경우, Test전체가 공유하는 객체를 만들기도 한다.
- 이 때도 아래의 것이 필요
- 테스트의 실행순서가 결과에 영향을 미치지 않을 것
- 문제는, Junit이 매번 테스트 클래스의 객체를 새로 만든다.
- 따라서 인스턴스 변수 같은 객체 레벨에 Application Context를 저장해두면, 결국은 매 번 생성하게 된다.
- static 필드 + @BeforeClass 라는 static method 지원 ( 클래스 전체에 걸쳐 딱 한번만 실행)
- 이 메소드에서 “static 변수” 에 application context를 생성 및 저장 해 둘 수도 있다.
- 가장 좋은 방법은 → Spring이 직접 제공하는application context 지원 기능
Test를 위한 Application Context 관리
Junit을 이용하는 “테스트 context framework” 를 제공
- 그 테스트에서 필요로 하는, 그리고 모든 테스트가 공유할 수 있는 application context를 만들어 제공
@RunWith(SpringJunit4ClassRunner.class)
// test context가 자동으로 만들어줄 application context의 위치를 지정
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest{
private UserDao userDao;
...
@Autowired
private ApplicationContext context;
..
@Before
public void setUp(){
this.dao = this.context.getBean(”userDao”,UserDao.class);
System.out.println(this); // 매 번 다른 참조값 (테스트 메소드마다 새로운 객체 생성)
System.out.println(this.context);// 같은 참조값 ( 테스트들이 공유하는 context)
..
}
}
- @Autowired 는 어플리케이션 컨텍스트로부터 “ 같은 타입의 bean(+bean 이름을보기도)” 을 가져오는 것인데, ApplicationContext 자신을 자동와이어링 하는게 가능한가?
- Spring Application Context는 “초기화시 자신도 bean으로 등록” 하기 때문.
- 그리고 사실 위처럼 dao를 받아오는 경우라면 애초부터 @Autowired UserDato userDao; 를 할 수도 있음.
- @Runwith ? ( ✋ 참고로 Junit5에서는 Extend어쩌고를 써야하는 것 같았음)
- Junit 프레임워크의 “테스트 실행방법 확장” 시에 사용
- SpringJunit4ClassRunner 라는 클래스를 지정해주면 JUnit이 테스트를 진행하는 중 테스트가 사용할 컨텍스트를 만들고 관리하는 작업을 진행해줌.
- @ContextConfiguration : 자동으로 만들어줄 어플리케이션 컨텍스트의 설정파일 위치 지정
미리 생성해 둔, application context 를, 테스트 오브젝트의 필드에 주입 해 주는 것.
- 첫 번째 테스트 실행 시 최초로 생성하기에 가장 오랜 시간이 걸린다.
- 이렇게 하나의 테스트 클래스 내의 테스트 메소드들은, 같은 application context 공유 및 사용 가능
테스트 “클래스 “ 간의 컨텍스트 공유
Spring은 테스트 클래스 사이에서도 컨텍스트 공유를 하게 해 준다. 두 테스트 클래스가 같은 설정파일을 사용 한다면 이런것이 가능
@RunWith(SpringJunit4ClassRunner.class)
// test context가 자동으로 만들어줄 application context의 위치를 지정
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest{ ..}
@RunWith(SpringJunit4ClassRunner.class)
// test context가 자동으로 만들어줄 application context의 위치를 지정
@ContextConfiguration(locations="/applicationContext.xml")
public class ItemDaoTest{
- 즉, 설정파일(__.xml) 종류만큼 application context를 만들고, 같은 설정파일을 지정한 테스트에선 이를 공유하게 해준다.
같은 타입의 빈이 2개 이상
예를들어 DataSource 타입(interface), MySqlDataSource 타입( implements DataSource) 의 빈이 등록되어있는 경우
Test에서 받아올 빈은 어떤 타입으로 선언해 두는게 좋을까?
- DataSource : 왠만하면 이 타입으로 선언하는 게 좋다 → dataSource 빈의 구현클래스를 변경하더라도, 테스트 코드를 수정할 필요가 없어지므로.
- MySqlDataSource 타입 : MySqlDataSource 타입 객체 자체에 관심이 있는 경우 ( 즉, 구현을 확인한다던가..)
“느슨한 결합” 을 하는게 좋기 때문에, 가능한 테스트에서도 “인터페이스를 사용” 하자
인터페이스, DI 를 사용해야 하는 이유
- 변경 가능성
- 확장 가능성
- 테스트 🤔
- 테스트는 자동으로 실행가능 && 빠르게 동작하도록 해야한다. ⇒ 따라서 가능한 작은 단위의 대상에 국한해 테스트 해야함.
- DI 는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되도록 하는데 중요한 역할 이다. 뒤에 내용 보면 좀 더 이해가 될 것
1️⃣테스트 코드에 의한! DI
이제까지야 “프레임워크에 의한 DI” 를 보았음.
그런데 프레임워크의 도움 없이, Factory를 통한 DI도 얼마든지 있음**. 즉 직접 DI를 해 줄 수 있다**는 것.
- 그런데 이것이 가능하려면, setter 와 같은 수정자 메소드를 열어둬야 한다. →이를 열어두면, 테스트코드에서도 setter를 통해 직접 DI를 할 수 있게 된다.
- DI 에는 1. 수정자 주입, 2. 생성자 주입 3. 필드 주입 (이건 @Autowired)
- 즉, UserDao 에서 사용하는 DataSource 객체를 테스트코드에서 변경이 가능해짐.
예를들면
- applicationContext.xml 의 DataSource bean : 실제 서버의 DB pool서비스와 연결→”운영용! DB connection”을 리턴해줌 → 이를 test에서 사용해도 될까?? → 당연히 매우 위험..... 롤백이 안되면 어떡하나
- 테스트할 때는 xml파일의 DatSource를 수정(테스트용으로) 하고, 운영시에는 다시 운영용 DataSource로 수정 ? —> 번거롭고, 수정을 안할 경우 매우 위험...
- 따라서! 테스트 코드에 의한 DI가 필요한 것. ( 매 번 config파일을 바꿔 줄 수도(매 번 실제 오브젝트 관계를 재구성 하거나 ), 실제 운영용 리소스를 사용할 수도 없으니! )
- 그런데 이것도 주의해서 사용해야함
현재 상황 : 모든 테스트에서, 하나의 application context를 공유하는 상황 → 한 번 변경하면, 나머지 모든 테스트의 수행에서 변경된 application context가 계속 사용됨 → 좋지 않은 상황 : 테스트 중 변경한 context가 뒤의 테스트에 영향을 주게 되는 상황
- 따라서 @DirtiesContext 라는 annotation이 있다.
- 스프링의 테스트 컨텍스트 프레임워크에게, 해당 클래스의 test에서, application context의 상태를 변경한다는 것을 notify 해 줌.
- 그러면 “테스트 컨텍스트” 에서는, 이 annotation이 붙은 test class에는, application context 공유를 허용 x —> 매 번 새로운 application context를 생성 → 변경된 context가 다음 테스트에 영향 x
- 메소드 레벨에 사용 하는 경우 → 해당 메소드 실행이 끝나면, 변경된 application context는 폐기 → 이후 테스트들을 위한 새로운 application context 생성
- 하지만 이로인해, 매 번 application context를 생성 한다는 사실이 찜찜하다..
2️⃣테스트를 위한 별도의 DI 설정 파일
방금 본 것 처럼, 수동으로 DI하는 방법은 많은 단점을 가짐 → 코드 증가, 매 번 application context를 새로 생성.
- 아예 테스트 전용 설정파일을 따로 만들자.
- 그리고 test에서는, 이 설정파일만 사용하도록 하면 된다. ( 책의 예시는 xml이니까..)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml")
public class UserDatoTest{...}
3️⃣객체지향적 프로그래밍을 했다면 ,DI를 위해 컨테이너가 반드시 필요한 것은 아니다
- UserDatoTest의 관심사 : UserDao가 DAO로서 DB에 정보를 잘 등록하고 잘 가져오는지.
- 스프링 컨테이너를 사용하지 않고 테스트 작성이 가능하다.
- 즉, 테스트 코드에서 직접 오브젝트를 생성하고, DI 할 수 있다.
- 이것이 가능하려면***, UserDao 가 spring의 API에 의존하지 않고, “ 자신의 관심사에만 집중 “하여 깜끔하게 만들어진 코드여야만*** 한다. 이는 UserDao 를 작성 할 때 DI를 적용했기 때문.
- 🚀 DI는 객체지향 프로그래밍 스타일 ( dependency 를 가진 객체를 외부에서 주입 함 ⇒ 두 객체간의 느슨한 결합 ⇒ DI가 가능한 구조 ) =⇒ 테스트 하기 좋은 코드!!!🚀
- DI 컨테이너, 프레임워크는 DI를 편하게 적용하도록 도와줄 뿐. 이들이 DI를 가능하게 해 주는 것은 아님!!
// RunWith 같은 것도 안붙음
public class UserDaoTest{
UserDao dao; // Autowired한거아님
..
@Before
public void setUp(){
dao = new UserDao();
DataSource dataSource = new SuingleConnectionDataSource(....);
침투적(invasive)기술, 비침투적 기술
침투적 기술
- 어플리케이션 코드에, 기술관련 API가 등장 or 특정 인터페이스나 클래스를 사용하도록 강제하는 기술
- 코드가 해당 기술에 종속된다
비침투적 기술
- 어플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능
- 기술에 종속적이지 않은 코드를 유지할 수 있게 한다.
스프링은 비침투적 기술의 예 → 컨테이너 없는 DI테스트도 가능한 것
DI를 이용한 테스트 방법 세가지중 무엇을 선택?
- 항상 고려할 부분 : 스프링 컨테이너 없이 테스트할 수 있는 방법이 우선!!
- 테스트 수행속도가 빠르고, 테스트가 간결.
- 테스트를 위해 필요한 객체 생성과 초기화가 단순한 것을 가장 먼저 고려.
- 여러 객체와 복잡한 의존관계를 가진 경우 → 테스트 전용 설정파일을 이용
- 설정파일을 따로 만들었더라도, 예외적인 의존관계가 존재하면 → 테스트코드로 수동DI해서 테스트하자
- 이 때는, test method나 클래스에 @DirtiesContext 어노테이션을 꼭 붙여줄 것.
학습 테스트
baeldung에 있는 글들을 읽다보면, 단순히 api를 사용하는 가이드인데도 @Test 를 하는 것을 본 적 있다.
내가 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용방법을 익히려는 것 !
테스트이나, 기능 검증을 하는게 아님
- 이를 통해 얻을 수 있는 것 : 어설프가 알고있거나, 오해하고 있던 지식을 테스트만드는 과정을 통해 바로잡을 수 있다.
- 자동화된 테스트 코드 → 따라서 ,다양한 조건에 따라 기능이 어떻게 동작하는지 빠르게 확인 가능
- 다양한 조건을 모두 테스트로 남겨놓을 수가 있음.
- 학습 테스트 코드를 개발 중에 참고가능
- 다양한 기능, 조건에 대한 테스트 코드를 만들고 남겨둘 수 있다 → 개발 중 샘플코드로 참고 가능.
- 프레임워크 or 제품을 업그레이드 시 호환성 검증을 도와줌.
- 업그레이드 시에는 항상 문제가 발생할 수도 있음 → 테스트에만 먼저 적용해본다면 , 버그 발생 시, 업그레이드 일정을 늦추는 등 계획 수정이 가능.
- 물론 이는, 어플리케이션 주요 기능에 대한 학습 데이터를 충분히 만들어 놓은 경우에 가능
테스트 예시
Junit은 테스트 메소드를 수행할 때 마다 새로운 오브젝트를 만든다??(완료)
각 테스트 메소드를 실행할 때 마다 생성되는 테스트 오브젝트들 사이에, 어느것도 중복되지 않음을 검증 하는 테스트를 할 수 있다.
스프링 “테스트 컨텍스트” 는 한개만 만들어져 모든 테스트에서 공유되는가 ?(완료)
JUnit과 반대로, Spring의 “테스트용 application context”는 테스트 개수에 상관없이 한개만 만들어진다. 그리고 이렇게 만들어진 context는 모든 테스트에서 공유된다.
이를 테스트 해 보자 :
- 설정파일 생성
- test class에 설정파일 어노테이션옵션에 설정
Spring Test Context 와 Context caching
Spring5에 도입된 **SpringExtension** 은 Spring TestContext 를 JUnit 5 Jupiter text와 통합하기 위해 사용된다.
JUnit 5 Jupiter @ExtendWith annotation과 함께 사용됨. 이런식으로
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = WebTestConfig.class)
public class JunitTest {
- @ContextConfiguration → “클래스 레벨” 메타데이터를 정의함. 이는 통합테스트를 위함 ApplicationContext 에 대한 설정정보.
- Spring 3.1 이전까지는 XML 설정 파일 같은 path-based resource location만 지원되었음.
- Spring 3.1 부터는 context loader는 path-based 리소스(XML 리소스) 뿐만 아니라 **class-based 리소스(@Configuration 리소스 )**도 지원하기 시작하였음.
- Spring 4.0 부터는 context loader는 이 두가지 방법을 “동시에 지원”하기 시작함.
- 결과적으로 @ContextConfiguration 은 둘 다에 사용될 수 있는데
- path-based인 경우에는 (locations = .. ) 속성을
- component class를 통한 경우에는 (clasees = .. ) 속성을 사용한다.
- ContextLoader strategy 를 같이 선언해 줄 수 있는데, 명시적으로 loader를 설정하지 않아도, 기본 로더가 지원되기 때문.
- Spring TestContext 프레임워크에서는 test 들 사이에서 application context를 공유하는 것을 가능하게 한다고 했다.( 따라서 , ApplicationContext를 autowired 받아 값을 확인하면 모두 같은 값을 가졌음 - 위의 실습 테스트에서 진행 )( JUnit test 메소드들은, 실행할 때 마다 객체를 생성한다. ) 어떻게 이러한 것이 가능한걸까??로드된 context 에대한 캐싱을 지원하는 것은 중요하다. 왜냐하면 테스트를 시작하는데 걸리는 시간은 꽤나 중요한 문제이기 때문이다. Context가 복잡하고 클 수록, 매 번 테스트메소드를 수행할 때마다 context를 생성한다면 오랜 시간이 걸릴 것이다.
- 찾아보니 스프링에서는 테스트를 위한 Context Caching 을 지원한다고 한다.
- 그렇다면 Context caching은 어떻게 가능할까??여기에 사용되는 “설정 파라미터”들은 docs를 확인해보자Testing예를들어 아래와 같은 상황이었다면, 오로지 locations 정보에 의해서만 식별되는 key가 생성 되었을 것이다. 이런 클래스를 A,B를 두고 Test를 실행하면
테스트들 사이에서도 Application Context가 공유되고 있음을 확인할 수 있다.SameApplicationContextTestB applicationContext = org.springframework.context.support.GenericApplicationContext@379614be, started on Thu Jan 27 20:36:15 KST 2022 SameApplicationContextTestA applicationContext = org.springframework.context.support.GenericApplicationContext@379614be, started on Thu Jan 27 20:36:15 KST 2022
- @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = WebTestConfig.class) @RequiredArgsConstructor public class SameApplicationContextTestB { @Autowired final ApplicationContext applicationContext; @Test public void isSame(){ System.out.println("SameApplicationContextTestB applicationContext = " + applicationContext); } }
- 아무튼 , 프레임워크에서, 해당 ApplicationContext 를 로드하면 , 해당 key에 대응하는 “static context cache”에 저장 해 둔다. → 참고: 말그대로 static 변수에 저장 → 따라서 테스트들이 서로 다른 프로세스에서 실행된다면, 프로세스들 사이에 static 캐시는 공유되지 않기 때문에 이런 캐싱 메커니즘이 무용지물이 되어버림. 따라서 캐싱 메커니즘의 이득을 누리려면, 모든 테스트는 같은 프로세스 내에서 실행되어야 한다. 빌드프레임워크를 통해 테스트를 실행시킬 때면, 그 빌드 프레임워크가 test들 사이에 fork(새로운 프로세스 생성)을 하지는 않는지 확인해줘야함. 예를들어 Maven Surefire plug-in의 forkMode가 “always” 로 설정되어있으면, TestContext 프레임워크에서는 test class들 사이에 application context를 cache 해 줄 수가 없음. 결과적으로 빌드 프로세스가 엄청나게 느려질 거임.
- Testing
- ApplicationContext 는 이를 로드하기 위해 사용된 “ 설정 파라미터들의 조합 “을 통해 유일하게 식별될 수 있다. 결과적으로 “설정 파라미터들의 조합” 을 사용하여 “ context cache key “를 생성한다.
@ExtendWith + SpringExtension in Spring 5 Test
- Junit4 의 코드와 섞어서 사용하는 것에 주의할 것 ( import 되는 것을 잘 관리하자 )
- 3rd party라이브러리가 몇가지 있는데 - assertJ가 가장 간결한 듯. hamcretst, assertj 등이 존재함
예시 ) 스프링이 생성한 빈 오브젝트는 싱글톤방식인가?
테스트에서 @Autowired 로 가져온 빈 객체 == 어플리케이션 컨섹스트.getBean() ?? (완료)
버그 테스트
마치 알고리즘에서 반례를 찾는 것과 같다. 예외상황, 버그가 만들어지는 상황에 대해 생각해보고 해당상황에 대한 테스트를 만들어 보는 것이다.
왜 필요할까???
- 테스트의 완성도를 높여준다.
- 버그에 대해 분석할 수 있다
- 버그를 테스트로 만들어 실패하게 하려면, 버그의 이유와 문제를 정확히 알아야 한다. 즉 버그를 효과적으로 분석이 가능하다.
- 코드를 보는 것만으로는 버그의 원인을 알 수 없는 경우, 해당 문제가 발생하는 가장 단순한 코드와 버그테스트를 만들자.
.
동등분할(equivalence paritioning)
같은 결과를 내는 값의 범위를 구분해, 각 대표 값을 테스트 하는 것.
어던 작업의 결과의 종류가 true, false, 예외발생 (세 가지)이라면, 각 결과를 내는 입력값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는게 좋음
경계값 분석(boundary value analysis)
에러는 동등분할 범위의 “경계” 에서 자주 발생한다는 특징
경계 근처 값을 이용해 테스트하는 것.
- 입력값이 숫자라면, 0 or 정수의 max min 으로 테스트 ㄹ자
정리
- 테스트는 “자동화”되어야 하고, 빠르게 실행 할 수 있어야 함.
- main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리함
- 테스트 결과는 일관성 있어야 한다.
- 환경 .or 테스트 실행순서에 따라 결과가 달라지면 안됨
- 테스트는 포괄적으로 작성해야함. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁨
- 코드 작성과 테스트 수행의 간격이 짧을 수록 효과적 ( 그래서 TDD를 강조하는 것 )
- 테스트 하기 쉬운 코드가 좋은 코드
- 테스트를 먼저 만들고 → 테스트를 성공시키는 코드를 만들어가는 TDD도 유용
- 테스트 코드도 리팩토링이 필요
- @Before, @After 을 사용해, 테스트 메소드들간의 공통 준비 작업과 정리작업을 처리 가능
- 스프링 text context framework를 이용 → 테스트 성능향상 가능 ( 하나의 Application context만을 생성하여 , 여러 테스트 메소드에서 공유하기 때문 - 테스트 실행시간이 감소 )
- 동일한 설정파일을 사용하는 테스트는 하나의 aplication context를 공유
- @Autowired를 사용해, context bean을 test object에 DI 가능
- 기술의 사용방법을 익히고 이해를 돕기 위한 학습 테스트를 작성하자
- 오류가 발견될 경우!!! 그에 대한 버그테스트를 만들어두면 유용하다.
'책 > 토비의스프링' 카테고리의 다른 글
DI , 왜 생성자 주입을 하라는 걸까? (0) | 2022.02.16 |
---|---|
02. 원칙과 패턴 (0) | 2022.02.13 |
관심사의 분리 ( ~1.3.3) (0) | 2022.02.09 |