【问题标题】:Spring value injection in mockito模拟中的弹簧值注入
【发布时间】:2012-03-01 20:52:43
【问题描述】:

我正在尝试为以下方法编写测试类

public class CustomServiceImpl implements CustomService {
    @Value("#{myProp['custom.url']}")
    private String url;
    @Autowire
    private DataService dataService;

我在类中的一种方法中使用注入的 url 值。 为了测试这一点,我编写了一个 junit 类

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext-test.xml" })
public CustomServiceTest{
    private CustomService customService;
    @Mock
    private DataService dataService;
    @Before
    public void setup() {
        customService = new CustomServiceImpl();
        Setter.set(customService, "dataService", dataService);
    }    
    ...
}

public class Setter {
    public static void set(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

在 applicationContext-test.xml 中,我正在使用

加载属性文件
    <util:properties id="myProp" location="myProp.properties"/>

但是在运行测试时,url 值没有加载到 CustomService 中。 我想知道是否有办法完成这项工作。

谢谢

【问题讨论】:

  • 如果你在嘲笑CustomService,那么CustomServiceImpl是如何被使用的呢?这没有意义。
  • 我正在使用CustomeServiceImpl。更新了代码。如何模拟需要从属性文件中读取的 url 的值?
  • 就目前而言,您没有使用 Spring 设置 customService 值,而是使用以下代码在 setup() 方法中手动设置值:customService = new CustomServiceImpl();
  • 不,我没有使用 spring 来设置测试的 customService 值,但在实际应用程序中,我使用 spring 来设置值。是否可以像我为 dataService 那样模拟 url 的值?

标签: java spring spring-mvc properties mockito


【解决方案1】:
import org.springframework.test.util.ReflectionTestUtils;

@RunWith(MockitoJUnitRunner.class)
public CustomServiceTest{

@InjectMocks
private CustomServiceImpl customService;

@Mock
private DataService dataService;

@Before
public void setup() {
    ReflectionTestUtils.setField(customService, "url", "http://someurl");
}    
...
}

【讨论】:

  • 您能否也用几句话解释您的解决方案?
  • 来自 Rohit 的问题可能还有其他问题,但是在为我的问题寻找解决方案时,这个解决方案正是我想要的,谢谢 Robert
  • 和groovy一样吗?
  • 是的,它在 groovy 中应该是相同的,除了 java 和 groovy 之间的一些细微语法差异。所有的 spring 库都应该可以在 groovy 项目中使用
  • @InjectMock 最后没有“s”?这东西是哪个包的?
【解决方案2】:

我同意@skaffman 的评论。

除了您的测试使用MockitoJUnitRunner,因此它不会寻找任何Spring 的东西,这唯一的目的是初始化Mockito 模拟。 ContextConfiguration 不足以用弹簧连接东西。从技术上讲,如果您想要与弹簧相关的东西,您可以使用 JUnit:SpringJUnit4ClassRunner

在您编写单元测试时,您可能需要重新考虑使用 spring。在单元测试中使用弹簧接线是错误的。但是,如果您改为编写集成测试,那么为什么要在那里使用 Mockito,这没有意义(正如 skaffman 所说)!

编辑:现在在您的代码中,您可以在 before 块中直接设置 CustomerServiceImpl,这也没有任何意义。那里根本不涉及 Spring!

@Before
public void setup() {
    customService = new CustomServiceImpl();
    Setter.set(customService, "dataService", dataService);
}

编辑 2:如果你想写一个CustomerServiceImpl单元测试,那么避免使用 Spring 的东西并直接注入属性的值。您也可以使用 Mockito 将 DataService 模拟直线注入测试实例。

@RunWith(MockitoJUnitRunner.class)
public CustomServiceImplTest{
    @InjectMocks private CustomServiceImpl customServiceImpl;
    @Mock private DataService dataService;

    @Before void inject_url() { customServiceImpl.url = "http://..."; }

    @Test public void customerService_should_delegate_to_dataService() { ... }
}

您可能已经注意到,我使用的是对url 字段的直接访问,该字段可以是包可见的。这是一个实际注入 URL 值的测试解决方法,因为 Mockito 仅注入模拟。

【讨论】:

  • “重新考虑使用spring”的意思是重新考虑使用Spring配置文件来为this test做依赖注入,对吧?你并不是建议他放弃在他的应用程序中使用 Spring。
  • @jhericks 不,我的意思是在单元测试中删除与 Spring 相关的代码。我通常对此持强硬态度:我不鼓励在任何单元测试中使用胶合代码,例如 Sprin。 单元测试 的目的是单独测试生产代码(此处为CustomerServiceImpl)。这实际上似乎是问题作者的意图。如果出于某种原因需要 spring,那么它开始是一个 集成测试,它在测试中没有相同的含义。
【解决方案3】:

您可以自动装配到 mutator(setter)中,而不仅仅是注释私有字段。然后你也可以从你的测试类中使用那个设置器。无需公开,包私有即可,因为 Spring 仍然可以访问它,但否则只有您的测试可以进入那里(或同一包中的其他代码)。

@Value("#{myProp['custom.url']}")
String setUrl( final String url ) {
    this.url  = url;
}

我不喜欢仅仅为了测试而以不同的方式(与我的代码库相比)自动装配,但是从测试中更改被测类的替代方法简直是邪恶的。

【讨论】:

  • 你知道这是否也适用于 @Resource 注释?
【解决方案4】:

你不应该嘲笑你正在尝试测试的东西。这是没有意义的,因为你不会接触任何你试图测试的代码。而是从上下文中获取CustomerServiceImpl 的实例。

【讨论】:

  • 不要犯单元测试的大罪。
【解决方案5】:

我有一个从属性文件中读取的字符串列表。 @Before 块中使用的 ReflectionTestUtils 类 setField 方法帮助我在执行测试之前设置这些值。即使对于依赖于 Common DaoSupport 类的我的 dao 层,它也能完美运行。

@Before
public void setList() {
    List<String> mockedList = new ArrayList<>();
    mockedSimList.add("CMS");
    mockedSimList.add("SDP");
    ReflectionTestUtils.setField(mockedController, "ActualListInController",
            mockedList);
}

【讨论】:

    【解决方案6】:

    您可以使用这个小实用程序类 (gist) 自动将字段值注入目标类:

    public class ValueInjectionUtils {
      private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
      private static final ConversionService CONVERSION_SERVICE = new DefaultConversionService();
      private static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
          new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX, SystemPropertyUtils.PLACEHOLDER_SUFFIX,
              SystemPropertyUtils.VALUE_SEPARATOR, true);
    
      public static void injectFieldValues(Object testClassInstance, Properties properties) {
        for (Field field : FieldUtils.getFieldsListWithAnnotation(testClassInstance.getClass(), Value.class)) {
          String value = field.getAnnotation(Value.class).value();
          if (value != null) {
            try {
              Object resolvedValue = resolveValue(value, properties);
              FieldUtils.writeField(field, testClassInstance, CONVERSION_SERVICE.convert(resolvedValue, field.getType()),
                  true);
            } catch (IllegalAccessException e) {
              throw new IllegalStateException(e);
            }
          }
        }
      }
    
      private static Object resolveValue(String value, Properties properties) {
        String replacedPlaceholderString = PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(value, properties);
        return evaluateSpEL(replacedPlaceholderString, properties);
      }
    
      private static Object evaluateSpEL(String value, Properties properties) {
        Expression expression = EXPRESSION_PARSER.parseExpression(value, new TemplateParserContext());
        EvaluationContext context =
            SimpleEvaluationContext.forPropertyAccessors(new MapAccessor()).withRootObject(properties).build();
        return expression.getValue(context);
      }
    }
    

    它使用org.apache.commons.lang3.reflect.FieldUtils 访问所有带有@Value 注释的字段,然后使用Spring 实用程序类来解析所有占位符值。如果您想使用自己的 PlaceholderResolver,您还可以将参数类型 properties 更改为 PlaceholderResolver。 在您的测试中,您可以使用它来注入一组作为MapProperties 实例给出的值,如下例所示:

    HashMap<String, Object> props = new HashMap<>();
    props.put("custom.url", "http://some.url");
    
    Properties properties = new Properties();
    properties.put("myProp", props);
    
    ValueInjectionUtils.injectFieldValues(testTarget, properties);
    

    这将尝试解析您的dataService 中的所有@Value 注释字段。我个人更喜欢这个解决方案而不是 ReflectionTestUtils.setField(dataService, "field", "value");,因为您不必依赖硬编码的字段名称。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-12-06
      • 1970-01-01
      • 2020-11-21
      • 1970-01-01
      • 2019-10-17
      • 2012-06-29
      • 2017-06-22
      • 2018-11-22
      相关资源
      最近更新 更多