事务性注释避免了服务被嘲笑



我有一个drools规则文件,它在规则中使用服务类。所以有一条规则是这样的:

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

在用@service和@Transactional(propagation=propagation.SUPPORTS)注释的validationservice中,drools文件在statelessKnowledgebase中使用,并添加应该在drools中使用的事实。一旦完成,就会调用session.execute(facts)并启动规则引擎。

为了测试规则,我想存根countryService.getCountryById()。使用mockito没有大问题。对其他使用流式设置的服务也这样做了,效果很好。然而,在这种特殊的情况下,countryService并没有受到阻碍,我不知道为什么。在花了很多时间检查我的代码后,我发现在服务之上有@Transactional或没有这个注释会有所不同。缺少@Transaction使mockito对countryservice进行了模拟而没有任何问题,有了@transactional导致mockito在注入mock时失败(没有任何错误或提示),因此使用了原始的countryservice对象。

我的问题是为什么这个注释会导致这个问题。为什么mockito在设置了@Transactional时不能注入mock?我注意到mockito失败了,因为当我调试和检查countryService时,当它作为全局添加到drowls会话中时,我在调试窗口中检查countryService时看到了以下差异:

  • @transactional:countryService的值为countryService$$EnhancerByCGLIB$$b80dbb7b

  • 没有@transactional:countryService的值为countryService$$EnhancerByMockitoWithCGLIB$$27f34dc1

除了在countryservice方法中找到@transactional我的断点getCountryById,调试器会在该断点处停止,但如果没有@transactionalmy断点,mockito会跳过它。

验证服务:

@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方法,你知道该怎么办吗?

问候,

Michael

请注意,自Spring 4.3.1以来,ReflectionTestUtils应自动打开代理。所以

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

现在应该工作,即使您的countryService@Transactional@Cacheable。。。(也就是说,在运行时隐藏在代理后面)

相关问题:SPR-14050

您的ValidationService被封装在JdkDynamicAopProxy中,因此当Mockito将mock注入服务时,它看不到任何可以注入的字段。你需要做两件事之一:

  • Forego启动您的Spring应用程序上下文并测试验证服务,强制您模拟每个依赖项
  • 或者从JdkDynamicAopProxy打开您的实现,然后自己处理注入mock

代码示例:

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

根据SuperSaiyen的回答,我创建了一个插件实用程序类,使其更简单&类型安全:

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

用法很简单,只需在测试方法的开头,用要注入mock的bean和应该被mock的对象的类调用方法mockFieldOnBean(Object beanToInjectMock, Class<T> classToMock)。示例:

假设您有一个类型为SomeService的bean,它包含一个SomeOtherService的自动连接bean,类似于;

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

要在SomeServicebean上模拟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();
}
}

一切都应该正常运转。

另一种解决方案是在Spring将所有内容连接在一起之前将mock对象添加到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注释非常重要,确保新的mock CountryService具有注入的最高优先级,而不是普通的注入优先级。然而,如果在多个地方注入类,这可能会产生意想不到的副作用。

在Spring测试模块中存在一个名为AopTestUtils的Spring实用程序。

public static <T> T getUltimateTargetObject(Object candidate)

获取所提供的候选对象的最终目标对象,不仅展开顶级代理,还展开任意数量的嵌套代理代理。如果提供的候选是Spring代理,则最终将返回所有嵌套代理的目标;否则候选人将按原样返回。

您可以在测试期间注入mock或spy并取消类的氧气,以安排mock或验证

相关内容

  • 没有找到相关文章