TGIF-STUDY / Toby-s-Spring

토비의 스프링 1권 궁금 모음집
1 stars 0 forks source link

[2장] 테스트 #3

Open her0807 opened 1 year ago

her0807 commented 1 year ago

테스트를 늘 작성하기는 하지만, 왜 작성해야하는지 알게 된 장

her0807 commented 1 year ago

스프링이 개발자에게 제공하는 가장 중요한 가치는 무엇인가?

p.145

스프링이 개발자에게 제공하는 가장 중요한 가치가 무엇이냐고 질문한다면 주저하지 않고 객체지향과 테스트라고 대답할 것이다. 

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

수동으로 테스트하는 것의 문제점

p.153

내가 느꼈던 수동 테스트의 문제점은 실행작업의 번거로움이다. 모든 케이스에 대해서 매번 테스트 할 수 없기도 하다. 예전에 프린트 해서 실행 값들을 프론트에서 찍어봤던 경험이 있는데, 실제 비지니스 코드에 차마 다 못지운 println() 들이 올라가서 곤욕을 치뤘던 경험이 있다. ㅋㅋ

her0807 commented 1 year ago

JUnit4 의 테스트 메소드는 왜 public이어야 할까요?

추츧하기에는 테스트 스텁 하나당 오브젝트가 만들어진다고 했다. 과거에는 다이나믹 동적 프록시를 사용하는게 기본 값이었기 때문에 그래서 각 실제 클래스를 상속 받은 프록시 객체를 만들어야했을 것이다. 그런데 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 메소드만 테스트로 허용하고 있습니다

her0807 commented 1 year ago

jnuit 에서 의존성 주입 방식이 @autowired 로 강제되는 이유

학습동기

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;
    }

결론부터 말씀드리자면 TestClass 에서는 하나의 의존성 주입 방식을 사용하고 있어요.

테스트 코드를 작성하기 위해 사용하는 라이브 러리인 Junit 에서는 스프링과 별개로 의존성 주입을 하고 있어요.그래서 위 코드에서 생성자를 만든건 생성자로 의존성을 주입하기 위해서가 아니었더라구요. 각각 @autowired 를 달아주는 번거로움을 해소하고자 @TestConstructor(autowireMode = TestConstructor.AutowireMode.*ALL*) 어노테이션으로 생성자 위에 autowired 를 달아준 것과 같은 효과를 내기 위해서 였어요.

@autowired 로 의존성을 주입하지 않으면 ParameterResolutionException 이 발생해요. 이유는 JUnit Engine의 Parameter Resolver 인터페이스에 의해 의존성 주입이 되기 때문인데요.

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;
  }
}

왜 Junit 5은 생성자 의존성 주입을 못할까?

Jupiter vs Spring 환경 차이 때문에!

  • 스프링 프레임워크의 경우 Spring Ioc 컨테이너가 등록할 Bean 들을 먼저 찾아서 보관하고 있어요
  • 이후 생성자 주입을 요구하는경우, 적절한 Bean을 찾아서 생성자 주입을 수행하게 돼요
  • 테스트 프레임 워크의 경우 생성자 매개 변수 관리를 Jupiter가 하게됩니다
  • 생성자 주입을 요구하는경우, 생성자 매개변수를 처리할 ParameterResolver을 열심히 뒤져보게 되지만, 해당 빈은 스프링이 가지고 있기때문에 처리하지 못하게돼요
  • 이 때문에 나 못찾았어! 하고, ParameterResolutionException 에러가 발생하게 되는거죠!
  • 하지만 @AutoWired 어노테이션을 달아 명시해 주게 된다면, Jupiter가 빈 주입을 스프링 컨테이너에게 요청하게 되어서, 정상적으로 빈 주입을 받을 수 있게돼요 ㅎㅎ
  • 참고 자료

her0807 commented 1 year ago

junit 이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식

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

장점

주의사항

Q. 왜 테스트 메소드를 실행할 때마다 새로운 오브젝트를 만드는 것일까? 그냥 테스트 클래스마다 하나의 오브젝트만 만들어놓고 사용하는 편이 성능도 낫고 더 효율적이지 않을까?

her0807 commented 1 year ago

@DirtiesContext 동작원리와 내 의견

DirtiesContext 를 사용하면 아주 간편하게 데이터 격리가 가능하다. 그래서 참 편리한것 같다. 그런데 수백개가 넘는 통합 테스트, 단위테스트에 DirtiesContext 를 사용하게 되면 매번 컨테이너 초기화가 일어나고 모든 빈을 재주입 하는 과정을 거치다보니 테스트 실행에 많은 시간이 소요된다. 그래서 사용하고 싶은 방법은 아니다.

그렇다면 테스트 격리는 어떤 방법으로 해야하는가?

her0807 commented 1 year ago

테스트를 할 때 값 객체 선정 기준?

💬 경계값 분석(boundary value analysis)

에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다.

보통 숫자의 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.

her0807 commented 1 year ago

2.6 정리

her0807 commented 1 year ago

TestContaner

eejihoon commented 1 year ago

RandomUtils 써보세욤