Skip to content

Commit

Permalink
Fix spying on scoped beans with @SpyBean
Browse files Browse the repository at this point in the history
Previously, when spying on a scoped bean the creation of the spy
would be performed using the scoped proxy. This would result in
the spy being unable to spy on any of the target bean's methods as
the scoped proxy's implementations of those methods would be final.

This commit updates MockitoPostProcessor so that the creation of the
spy and injection of the @SpyBean-annotated field is performed using
the scoped target. The scoped target has not be proxied so this
allows Mockito to spy on all of its methods.

Closes gh-17817
  • Loading branch information
wilkinsona committed Aug 23, 2019
1 parent 0fb0eb6 commit 52050c1
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Set;
import java.util.TreeSet;

import org.springframework.aop.scope.ScopedObject;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
Expand Down Expand Up @@ -359,6 +360,9 @@ private void inject(Field field, Object target, String beanName) {
Assert.state(ReflectionUtils.getField(field, target) == null,
() -> "The field " + field + " cannot have an existing value");
Object bean = this.beanFactory.getBean(beanName, field.getType());
if (bean instanceof ScopedObject) {
bean = ((ScopedObject) bean).getTargetObject();
}
ReflectionUtils.setField(field, target, bean);
}
catch (Throwable ex) {
Expand Down Expand Up @@ -423,8 +427,9 @@ private static BeanDefinition getOrAddBeanDefinition(BeanDefinitionRegistry regi
}

/**
* {@link BeanPostProcessor} to handle {@link SpyBean} definitions. Registered as a
* separate processor so that it can be ordered above AOP post processors.
* {@link BeanPostProcessor} to handle {@link SpyBean @SpyBean} definitions.
* Registered as a separate processor so that it can be ordered above AOP post
* processors.
*/
static class SpyPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements PriorityOrdered {

Expand All @@ -443,15 +448,22 @@ public int getOrder() {

@Override
public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName);
return this.mockitoPostProcessor.createSpyIfNecessary(bean, getOriginalBeanNameIfScopedTarget(beanName));
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FactoryBean) {
if (bean instanceof FactoryBean || bean instanceof ScopedObject) {
return bean;
}
return this.mockitoPostProcessor.createSpyIfNecessary(bean, beanName);
return this.mockitoPostProcessor.createSpyIfNecessary(bean, getOriginalBeanNameIfScopedTarget(beanName));
}

private String getOriginalBeanNameIfScopedTarget(String beanName) {
if (ScopedProxyUtils.isScopedTarget(beanName)) {
return beanName.substring("scopedTarget.".length());
}
return beanName;
}

public static void register(BeanDefinitionRegistry registry) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.test.mock.mockito;

import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.boot.test.mock.mockito.SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests.SpyBeanOnTestFieldForExistingScopedBeanConfig;
import org.springframework.boot.test.mock.mockito.example.ExampleService;
import org.springframework.boot.test.mock.mockito.example.ExampleServiceCaller;
import org.springframework.boot.test.mock.mockito.example.SimpleExampleService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;

/**
* Test {@link SpyBean @SpyBean} on a test class field can be used to replace existing
* scoped beans.
*
* @author Andy Wilkinson
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = SpyBeanOnTestFieldForExistingScopedBeanConfig.class)
public class SpyBeanOnTestFieldForExistingScopedBeanIntegrationTests {

@SpyBean
private ExampleService exampleService;

@Autowired
private ExampleServiceCaller caller;

@Test
public void testSpying() {
assertThat(this.caller.sayGreeting()).isEqualTo("I say simple");
verify(this.exampleService).greeting();
}

@Configuration
@Import({ ExampleServiceCaller.class })
static class SpyBeanOnTestFieldForExistingScopedBeanConfig {

@Bean
@Scope(scopeName = "custom", proxyMode = ScopedProxyMode.TARGET_CLASS)
SimpleExampleService simpleExampleService() {
return new SimpleExampleService();
}

@Bean
static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("custom", new org.springframework.beans.factory.config.Scope() {

private Object bean;

@Override
public Object resolveContextualObject(String key) {
throw new UnsupportedOperationException();
}

@Override
public Object remove(String name) {
throw new UnsupportedOperationException();
}

@Override
public void registerDestructionCallback(String name, Runnable callback) {
throw new UnsupportedOperationException();
}

@Override
public String getConversationId() {
throw new UnsupportedOperationException();
}

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
if (this.bean == null) {
this.bean = objectFactory.getObject();
}
return this.bean;
}

});
return configurer;
}

}

}

0 comments on commit 52050c1

Please sign in to comment.