Open her0807 opened 1 year ago
p.145
스프링이 개발자에게 제공하는 가장 중요한 가치가 무엇이냐고 질문한다면 주저하지 않고 객체지향과 테스트라고 대답할 것이다.
애플리케이션은 계속 변하고 복잡해져간다. 그 변화에 대응하는 첫번째 전략이 확정과 변화를 고려한 객체지향적 설계와 그것을 효과적으로 담아 낼 수 있는 IoC 와 DI 같은 기술이라면, 두번째 전략은 만들어진 코드를 확신할 수 있게 해주고 변화에 유연하게 대처할 수 있는 자신감을
주는 테스트 기술이다.
p.153
내가 느꼈던 수동 테스트의 문제점은 실행작업의 번거로움이다. 모든 케이스에 대해서 매번 테스트 할 수 없기도 하다.
예전에 프린트 해서 실행 값들을 프론트에서 찍어봤던 경험이 있는데, 실제 비지니스 코드에 차마 다 못지운
println()
들이 올라가서 곤욕을 치뤘던 경험이 있다. ㅋㅋ
추츧하기에는 테스트 스텁 하나당 오브젝트가 만들어진다고 했다. 과거에는 다이나믹 동적 프록시를 사용하는게 기본 값이었기 때문에 그래서 각 실제 클래스를 상속 받은 프록시 객체를 만들어야했을 것이다. 그런데 private 메서드는 상속이 안되기 때문에 그런가 아닐까?
[토비의 티키타카] https://groups.google.com/g/ksug/c/xpJpy8SCrEE
요약 평소에 그냥 원래 그러려니 하고 생각없이 사용하던 것도 한번쯤 왜 그렇게 했을까 의문을 가져보고 생각을 해보는 것은 좋은 습관인듯 합니다.
스프링도 그런 태도를 가지고 기존 기술에 대한 의문을 가지고 고민을 하면서 만들어진 것일테니까요.
JUnit의 개발자들이 밝혔던 테스트 메소드가 public이어야 했던 이유가 있습니다. JUnit은 테스트 클래스의 테스트 메소드를 자바의 reflection api를 이용해서 호출을 해줍니다. Pluggable selector 패턴을 이용했기 때문에 그렇게 했죠. 그런데 JUnit이 처음 만들어지던 때 사용했던 JDK1.1에서는 리플렉션에서 public 메소드만 접근을 허용했습니다. 따라서 JUnit의 테스트 메소드는 모두 public일 수 밖에 없었습니다. 그게 관례가 되서 지금까지 내려온 것입니다.
그런데 JDK1.2부터는 리플렉션에서 public외의 모든 접근레벨을 다 허용하기 시작했습니다. 그래서 심지어 private메소드도 호출하는 것도 가능해졌죠. 심지어 final 변수의 값도 수정이 가능합니다(JDK5이후에는 미리 선언된 상수값으로 final인 경우 코드레벨 최적화 때문에 안되기도 합니다).
어쨌든 JUnit은 전통을 유지해서 JUnit 4.x에서도 여전히 public 메소드만 테스트로 허용하고 있습니다
prolog project test code 를 작성하다가 생성자 주입과 autowired 로 의존성 주입을 하는 방식 모두를 사용하고 있는 코드를 발견했어요. 왜 junit 을 사용하는 test 에서 두개의 의존성 주입 방식을 사용하게 된걸까요? 이전부터 궁금했던건데 테스트 코드에서는 의존성 주입 방식을 무조건 autowired 로 해야하는지도 의문이 들었어요.
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@NewIntegrationTest
class KeywordServiceTest {
private KeywordService keywordService;
private SessionRepository sessionRepository;
private KeywordRepository keywordRepository;
private EntityManager em;
public KeywordServiceTest(final KeywordService keywordService,
final SessionRepository sessionRepository,
final KeywordRepository keywordRepository,
final EntityManager em) {
this.keywordService = keywordService;
this.sessionRepository = sessionRepository;
this.keywordRepository = keywordRepository;
this.em = em;
}
테스트 코드를 작성하기 위해 사용하는 라이브 러리인 Junit 에서는 스프링과 별개로 의존성 주입을 하고 있어요.그래서 위 코드에서 생성자를 만든건 생성자로 의존성을 주입하기 위해서가 아니었더라구요. 각각 @autowired 를 달아주는 번거로움을 해소하고자 @TestConstructor(autowireMode = TestConstructor.AutowireMode.*ALL*)
어노테이션으로 생성자 위에 autowired 를 달아준 것과 같은 효과를 내기 위해서 였어요.
@autowired 로 의존성을 주입하지 않으면 ParameterResolutionException
이 발생해요. 이유는 JUnit Engine의 Parameter Resolver 인터페이스에 의해 의존성 주입이 되기 때문인데요.
어뎁터 페턴을 사용하여 상황별로 맞는 리졸버를 가리기 위한 인터페이스 입니다.
@SpringBootTest 에서 있는 SpringExtension.class 가 바로 Parameter Resolver 를 상속받아 구현되어 있어요.
public class SpringExtension implements ...ParameterResolver {
그리고 하위에 있는 supportsParameter 메서드에서는 isAutowirableConstructor 메서드로 어노테이션을 확인하구요.
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
...
return TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) || ApplicationContext.class.isAssignableFrom(parameter.getType()) || this.supportsApplicationEvents(parameterContext) || ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex());
}
더 내부에서는 testConstructor 를 통해서도 Autowired 를 확인해줍니다!
public static boolean isAutowirableConstructor(Constructor<?> constructor, Class<?> testClass, @Nullable PropertyProvider fallbackPropertyProvider) {
if (AnnotatedElementUtils.hasAnnotation(constructor, Autowired.class)) {
return true;
} else {
AutowireMode autowireMode = null;
TestConstructor testConstructor = (TestConstructor)TestContextAnnotationUtils.findMergedAnnotation(testClass, TestConstructor.class);
if (testConstructor != null) {
autowireMode = testConstructor.autowireMode();
} else {
String value = SpringProperties.getProperty("spring.test.constructor.autowire.mode");
autowireMode = AutowireMode.from(value);
if (autowireMode == null && fallbackPropertyProvider != null) {
value = fallbackPropertyProvider.get("spring.test.constructor.autowire.mode");
autowireMode = AutowireMode.from(value);
}
}
return autowireMode == AutowireMode.ALL;
}
}
Jupiter vs Spring 환경 차이 때문에!
- 스프링 프레임워크의 경우 Spring Ioc 컨테이너가 등록할 Bean 들을 먼저 찾아서 보관하고 있어요
- 이후 생성자 주입을 요구하는경우, 적절한 Bean을 찾아서 생성자 주입을 수행하게 돼요
- 테스트 프레임 워크의 경우 생성자 매개 변수 관리를 Jupiter가 하게됩니다
- 생성자 주입을 요구하는경우, 생성자 매개변수를 처리할 ParameterResolver을 열심히 뒤져보게 되지만, 해당 빈은 스프링이 가지고 있기때문에 처리하지 못하게돼요
- 이 때문에 나 못찾았어! 하고,
ParameterResolutionException
에러가 발생하게 되는거죠!- 하지만
@AutoWired
어노테이션을 달아 명시해 주게 된다면, Jupiter가 빈 주입을 스프링 컨테이너에게 요청하게 되어서, 정상적으로 빈 주입을 받을 수 있게돼요 ㅎㅎ참고 자료
장점
주의사항
Q. 왜 테스트 메소드를 실행할 때마다 새로운 오브젝트를 만드는 것일까? 그냥 테스트 클래스마다 하나의 오브젝트만 만들어놓고 사용하는 편이 성능도 낫고 더 효율적이지 않을까?
DirtiesContext 를 사용하면 아주 간편하게 데이터 격리가 가능하다. 그래서 참 편리한것 같다. 그런데 수백개가 넘는 통합 테스트, 단위테스트에 DirtiesContext 를 사용하게 되면 매번 컨테이너 초기화가 일어나고 모든 빈을 재주입 하는 과정을 거치다보니 테스트 실행에 많은 시간이 소요된다. 그래서 사용하고 싶은 방법은 아니다.
그렇다면 테스트 격리는 어떤 방법으로 해야하는가?
💬 경계값 분석(boundary value analysis)
에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다.
보통 숫자의 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.
main()
을 이용하지 말고, JUnit
프레임워크를 이용하면 테스트 자동화가 가능하다.@BeforeEach
, @AfterEach
를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다.@Autowired
를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI할 수 있다.TestContaner
RandomUtils 써보세욤
테스트를 늘 작성하기는 하지만, 왜 작성해야하는지 알게 된 장