복붙노트

[SPRING] 트랜잭션 주석은 서비스가 조롱되는 것을 방지합니다.

SPRING

트랜잭션 주석은 서비스가 조롱되는 것을 방지합니다.

규칙에 서비스 클래스를 사용하는 drools 규칙 파일이 있습니다. 그래서 하나의 규칙은 다음과 같이합니다 :

eval (countryService.getCountryById (1)! = null)

@service와 @Transactional (propagation = Propagation.SUPPORTS)으로 주석 된 유효성 검사 서비스에서, drools 파일은 statelessKnowledgebase에 사용되며, 잠김에서 사용해야하는 사실이 추가됩니다. 일단 이것이 끝나면 session.execute (사실)가 호출되고 룰 엔진이 시작됩니다.

규칙을 테스트하기 위해 countryService.getCountryById ()를 스텁하고 싶습니다. mockito를 사용하면 큰 문제는 없습니다. drools 설정을 사용하는 다른 서비스에도이 작업을 수행하면 정상적으로 작동합니다. 그러나이 특별한 경우 countryService는 스터브되지 않았으며 이유를 파악할 수 없었습니다. 많은 시간을 보내고 내 코드를 확인한 후에 서비스 위의 @Transactional을 사용하거나이 주석이 부족하다는 점을 발견했습니다. @Transaction이 없다면 mockito가 문제없이 countryservice를 모의하게 만들었고, @transactional을 제자리에두면 mockito가 원래의 countryservice 객체가 사용되도록 mockito를 삽입하는 데 오류나 힌트없이 실패하게되었습니다.

내 질문은이 주석으로 인해이 문제가 발생하는 이유입니다. @Transactional이 설정되었을 때 mockito가 mockito를 삽입 할 수없는 이유는 무엇입니까? moockito가 디버깅 할 때 countryService를 검사 할 때 실패한 것으로 나타났습니다. 내 debugwindow에서 countryservice를 검사 할 때 drools 세션에 전역으로 추가 될 때 다음과 같은 차이가 있습니다.

또한 @transactional과 함께 countryservice 메서드의 내 중단 점 getCountryById가 발견되고 디버거가 해당 중단 점에서 중지되지만 @transactional이 없으면 mockito가이를 건너 뛰므로 내 중단 점을 건너 뜁니다.

ValidationService :

@Service
@Transactional(propagation=Propagation.SUPPORTS)
public class ValidationService 
{
  @Autowired
  private CountryService countryService;

  public void validateFields(Collection<Object> facts)
  {
    KnowledgeBase knowledgeBase = (KnowledgeBase)AppContext.getApplicationContext().getBean(knowledgeBaseName); 
    StatelessKnowledgeSession session = knowledgeBase.newStatelessKnowledgeSession();
    session.setGlobal("countryService", countryService);
    session.execute(facts);

  }

그리고 테스트 클래스 :

public class TestForeignAddressPostalCode extends BaseTestDomainIntegration
{

  private final Collection<Object> postalCodeMinLength0 = new ArrayList<Object>();

  @Mock
  protected CountryService countryService;

  @InjectMocks
  private ValidationService level2ValidationService;


  @BeforeMethod(alwaysRun=true)
  protected void setup()
  {
    // Get the object under test (here the determination engine)
    level2ValidationService = (ValidationService) getAppContext().getBean("validationService");
    // and replace the services as documented above.
    MockitoAnnotations.initMocks(this);

    ForeignAddress foreignAddress = new ForeignAddress();
    foreignAddress.setCountryCode("7029");
    foreignAddress.setForeignPostalCode("foreign");

    // mock country to be able to return a fixed id
    Country country = mock(Country.class);
    foreignAddress.setLand(country);
    doReturn(Integer.valueOf(1)).when(country).getId();

    doReturn(country).when(countryService).getCountryById(anyInt());

    ContextualAddressBean context = new ContextualAddressBean(foreignAddress, "", AddressContext.CORRESPONDENCE_ADDRESS);
    postalCodeMinLength0.add(context);
  }

  @Test
  public void PostalCodeMinLength0_ExpectError()
  {
    // Execute
    level2ValidationService.validateFields(postalCodeMinLength0, null);

  }

이 @transactional 주석을 유지하고 countryservice 메소드를 스텁 (stub) 할 수 있다면 어떤 생각을해야할까요?

문안 인사,

남자 이름

해결법

  1. ==============================

    1.무슨 일이 일어나는가 ValidationService가 JdkDynamicAopProxy에 래핑되고 있기 때문에 Mockito가 서비스에 mockito를 삽입하려고 할 때이를 주입 할 필드가 보이지 않습니다. 다음 두 가지 중 하나를 수행해야합니다.

    무슨 일이 일어나는가 ValidationService가 JdkDynamicAopProxy에 래핑되고 있기 때문에 Mockito가 서비스에 mockito를 삽입하려고 할 때이를 주입 할 필드가 보이지 않습니다. 다음 두 가지 중 하나를 수행해야합니다.

    코드 예제 :

    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        ValidationService validationService = (ValidationService) unwrapProxy(level2ValidationService);
        ReflectionTestUtils.setField(validationService, "countryService", countryService);
    }
    
    public static final Object unwrapProxy(Object bean) throws Exception {
        /*
         * If the given object is a proxy, set the return value as the object
         * being proxied, otherwise return the given object.
         */
        if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
            Advised advised = (Advised) bean;
            bean = advised.getTargetSource().getTarget();
        }
        return bean;
    }
    

    문제에 대한 블로그 항목

  2. ==============================

    2.ReflectionTestUtils는 Spring 4.3.1부터 프록시를 자동으로 unwrap해야한다는 점에 유의하십시오. 그래서

    ReflectionTestUtils는 Spring 4.3.1부터 프록시를 자동으로 unwrap해야한다는 점에 유의하십시오. 그래서

    ReflectionTestUtils.setField(validationService, "countryService", countryService);
    

    귀하의 countryService에 @Transactional, @Cacheable ... (즉, 런타임시 프록시 뒤에 숨겨 짐) 주석이 달린 경우에도 작동해야합니다.

    관련 문제 : SPR-14050

  3. ==============================

    3.SuperSaiyen의 답을 바탕으로, 나는 더 간단하고 안전한 타입을위한 drop-in 유틸리티 클래스를 만들었습니다 :

    SuperSaiyen의 답을 바탕으로, 나는 더 간단하고 안전한 타입을위한 drop-in 유틸리티 클래스를 만들었습니다 :

    import org.mockito.Mockito;
    import org.springframework.aop.framework.Advised;
    import org.springframework.aop.support.AopUtils;
    import org.springframework.test.util.ReflectionTestUtils;
    
    @SuppressWarnings("unchecked")
    public class SpringBeanMockUtil {
      /**
       * If the given object is a proxy, set the return value as the object being proxied, otherwise return the given
       * object.
       */
      private static <T> T unwrapProxy(T bean) {
        try {
          if (AopUtils.isAopProxy(bean) && bean instanceof Advised) {
            Advised advised = (Advised) bean;
            bean = (T) advised.getTargetSource().getTarget();
          }
          return bean;
        }
        catch (Exception e) {
          throw new RuntimeException("Could not unwrap proxy!", e);
        }
      }
    
      public static <T> T mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock) {
        T mocked = Mockito.mock(classToMock);
        ReflectionTestUtils.setField(unwrapProxy(beanToInjectMock), null, mocked, classToMock);
        return mocked;
      }
    }
    

    사용법은 간단합니다. 테스트 메소드를 시작할 때 mockFieldOnBean (Object beanToInjectMock, Class classToMock) 메소드를 mock을 삽입하려는 bean과 조롱해야하는 객체의 클래스로 호출하면됩니다. 예:

    SomeOtherService의 Autowired Bean을 보유하고있는 SomeService 타입의 Bean을 가지고 있다고 가정 해 봅시다.

    @Component
    public class SomeService {
      @Autowired
      private SomeOtherService someOtherService;
    
      // some other stuff
    }
    

    SomeService bean에서 someOtherService를 조롱하려면 다음을 사용하십시오.

    @RunWith(SpringJUnit4ClassRunner.class)
    public class TestClass {
    
      @Autowired
      private SomeService someService;
    
      @Test
      public void sampleTest() throws Exception {
        SomeOtherService someOtherServiceMock = SpringBeanMockUtil.mockFieldOnBean(someService, SomeOtherService.class);
    
        doNothing().when(someOtherServiceMock).someMethod();
    
        // some test method(s)
    
        verify(someOtherServiceMock).someMethod();
      }
    }
    

    모든 것이 제대로 작동해야합니다.

  4. ==============================

    4.또 다른 해결책은 스프링이 모든 것을 연결하기 전에 모의 객체를 Spring 컨텍스트에 추가하는 것입니다. 그러면 테스트가 시작되기 전에 이미 삽입되어있을 것입니다. 수정 된 테스트는 다음과 같이 보일 수 있습니다.

    또 다른 해결책은 스프링이 모든 것을 연결하기 전에 모의 객체를 Spring 컨텍스트에 추가하는 것입니다. 그러면 테스트가 시작되기 전에 이미 삽입되어있을 것입니다. 수정 된 테스트는 다음과 같이 보일 수 있습니다.

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { Application.class, MockConfiguration.class })
    public class TestForeignAddressPostalCode extends BaseTestDomainIntegration
    {
    
      public static class MockConfiguration {
    
          @Bean
          @Primary
          public CountryService mockCountryService() {
            return mock(CountryService.class);
          }
    
      }
    
      @Autowired
      protected CountryService mockCountryService;
    
      @Autowired
      private ValidationService level2ValidationService;
    
      @BeforeMethod(alwaysRun=true)
      protected void setup()
      {
    
        // set up you mock stubs here
        // ...
    

    @Primary 주석은 중요합니다. 새 모의 CountryService가 정상적인 주입을 대체하여 주입에 최우선 순위를 부여해야합니다. 그러나 클래스가 여러 위치에 주입되는 경우 의도하지 않은 부작용이있을 수 있습니다.

  5. from https://stackoverflow.com/questions/12857981/transactional-annotation-avoids-services-being-mocked by cc-by-sa and MIT license