如何使用Mockito在Spring中模拟带有@Value注解的自动装配字段?

267

我正在使用Spring 3.1.4.RELEASE和Mockito 1.9.5。在我的Spring类中,我有:

@Value("#{myProps['default.url']}")
private String defaultUrl;

@Value("#{myProps['default.password']}")
private String defaultrPassword;

// ...

从我的JUnit测试来看,我目前已经设置好了如下所示:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class MyTest 
{ 

我希望模拟我的“defaultUrl”字段的值。 请注意,我不想为其他字段模拟值 - 我只想保持它们原样,只改变“defaultUrl”字段。 还请注意,我的类中没有显式的“setter”方法(例如setDefaultUrl ),也不想为测试创建任何这样的方法。

在这种情况下,我如何模拟那个字段的值?

10个回答

295
你可以使用Spring的ReflectionTestUtils.setField魔法来避免对你的代码进行任何修改。 Michał Stochmal的评论提供了一个例子:
在测试期间调用bean方法之前,使用ReflectionTestUtils.setField(bean, "fieldName", "value")
查看this教程获取更多信息,虽然该方法非常易于使用,你可能不需要这些信息。 更新: 自 Spring 4.2.RC1 版本以来,现在可以设置静态字段而无需提供类的实例。请参见文档中的 this 部分和 this 提交记录。

22
在链接失效的情况下,请在测试期间调用您的“bean”方法之前使用ReflectionTestUtils.setField(bean, "fieldName", "value");来设置“bean”对象的字段值。 - Michał Stochmal
4
从属性文件中检索属性的好方法是使用模拟(mocking)技术。 - Antony Sampath Kumar Reddy
1
@MichałStochmal,这样做会产生java.lang.IllegalStateException: Could not access method: Class org.springframework.util.ReflectionUtils can not access a member of class com.kaleidofin.app.service.impl.CVLKRAProvider with modifiers ""的错误,因为该字段是私有的。在org.springframework.util.ReflectionUtils类中无法访问com.kaleidofin.app.service.impl.CVLKRAProvider类的成员。 - Akhil Surapuram
当您想测试一个使用@Value("${property.name}")注解访问属性的类中的private变量时,这很有效。 - Ayush Kumar
我们如何使用Mockito来模拟@Value("#{${patientTypes}}") private Map<String, Integer> patientTypes; - PAA
显示剩余3条评论

178

这已经是我第三次通过谷歌搜索到这篇SO文章,因为我总是忘记如何模拟@Value字段。虽然被接受的答案是正确的,但我总是需要一些时间来正确地调用"setField",所以至少对于我自己,我在这里贴上一个示例片段:

生产类:

@Value("#{myProps[‘some.default.url']}")
private String defaultUrl;

测试类:

import org.springframework.test.util.ReflectionTestUtils;

ReflectionTestUtils.setField(instanceUnderTest, "defaultUrl", "http://foo");
// Note: Don't use MyClassUnderTest.class, use the instance you are testing itself
// Note: Don't use the referenced string "#{myProps[‘some.default.url']}", 
//       but simply the FIELDs name ("defaultUrl")

谢谢。经过数小时的调试,它帮了我很大的忙。 - Chandan Kumar

71

你可以使用这个神奇的Spring测试注解:

@TestPropertySource(properties = { "my.spring.property=20" }) 

查看org.springframework.test.context.TestPropertySource

例如,这是测试类:

@ContextConfiguration(classes = { MyTestClass.Config.class })
@TestPropertySource(properties = { "my.spring.property=20" })
public class MyTestClass {

  public static class Config {
    @Bean
    MyClass getMyClass() {
      return new MyClass ();
    }
  }

  @Resource
  private MyClass myClass ;

  @Test
  public void myTest() {
   ...

这是带有属性的类:

@Component
public class MyClass {

  @Value("${my.spring.property}")
  private int mySpringProperty;
   ...

6
这应该是被接受的答案。我需要提醒自己一件事:需要模拟您使用的所有@Values,您不能仅模拟第一个并注入第二个属性。 - afe
这对我有用,但是如何仅为一个测试用例设置属性值,而不是整个测试类? - Kristijan Iliev
@KristijanIliev:是的,这个注释适用于整个测试类。如果你想使用不同的值,你必须手动更改它,例如使用ReflectionTestUtils.setField。 - Thibault
3
对于服务层的单元测试,我们通常使用JUnit5中的@ExtendWith(MockitoExtension.class)。在这些情况下,与ReflectionTestUtils.setField(bean, "fieldName", "value")解决方案相比,建议的方法可能更复杂,因为我们需要手动注入所有模拟对象而不是使用@InjectMocks。 更简单的解决方案是在被测试服务的构造函数中自动装配依赖项和值,并手动实例化要测试的服务 - Service serviceUnderTest = new ServiceImpl(Integer, String, RepositoryMock, OtherServiceMock) - Kostadin Mehomiyski

43

我建议另一个相关的解决方案,即将用@Value注释的字段作为构造函数的参数传递,而不是使用ReflectionTestUtils类。

不要这样做:

public class Foo {

    @Value("${foo}")
    private String foo;
}

public class FooTest {

    @InjectMocks
    private Foo foo;

    @Before
    public void setUp() {
        ReflectionTestUtils.setField(Foo.class, "foo", "foo");
    }

    @Test
    public void testFoo() {
        // stuff
    }
}

做这个:

public class Foo {

    private String foo;

    public Foo(@Value("${foo}") String foo) {
        this.foo = foo;
    }
}

public class FooTest {

    private Foo foo;

    @Before
    public void setUp() {
        foo = new Foo("foo");
    }

    @Test
    public void testFoo() {
        // stuff
    }
}

这种方法的好处是:1)我们可以不使用依赖容器(只需要构造函数)来实例化Foo类,2)我们没有将测试与实现细节耦合在一起(反射会将我们绑定到使用字符串的字段名称上,如果我们更改字段名称可能会导致问题)。


3
缺点:如果有人改动了注释,例如使用属性“bar”而不是“foo”,你的测试仍将正常运行。我只是遇到这种情况。 - Nils Rommelfanger
@NilsEl-Himoud,这是一个公正的观点,对于OP的问题来说一般都是如此,但你提出的问题无论是使用反射工具还是构造函数都没有更好或更糟。这个答案的重点是考虑使用构造函数而不是反射工具(被接受的答案)。Mark,感谢你的回答,我很欣赏这个微调的简洁和清晰。 - Marquee
@Marquee,你应该像这样将一个对象实例传递给setField: ReflectionTestUtils.setField(foo, "foo", "foo"); - Ousama
我喜欢这个解决方案! - Uvuvwevwevwe

37

你还可以将属性配置模拟到测试类中

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class MyTest 
{ 
   @Configuration
   public static class MockConfig{
       @Bean
       public Properties myProps(){
             Properties properties = new Properties();
             properties.setProperty("default.url", "myUrl");
             properties.setProperty("property.value2", "value2");
             return properties;
        }
   }
   @Value("#{myProps['default.url']}")
   private String defaultUrl;

   @Test
   public void testValue(){
       Assert.assertEquals("myUrl", defaultUrl);
   }
}

如果每个测试需要不同的配置,有没有办法使用它? - Frank Why

32

我也做了同样的事情,但仍然没有反映出来。 - not-a-bug
1
@Mendon Ashwini,链接已经失效,请修复。 - Ajay Takur

13

还要注意,我的类中没有任何显式的“setter”方法(例如setDefaultUrl),我也不想仅为测试目的而创建任何这样的方法。

一种解决方法是将您的类更改为使用构造函数注入,这可用于测试和Spring注入。不需要反射 :)

因此,您可以通过构造函数传递任何字符串:

class MySpringClass {

    private final String defaultUrl;
    private final String defaultrPassword;

    public MySpringClass (
         @Value("#{myProps['default.url']}") String defaultUrl, 
         @Value("#{myProps['default.password']}") String defaultrPassword) {
        this.defaultUrl = defaultUrl;
        this.defaultrPassword= defaultrPassword;
    }

}

在你的测试中,只需使用它:

MySpringClass MySpringClass  = new MySpringClass("anyUrl", "anyPassword");

我认为这是最好的答案,不过最好解释一下为什么在测试属性上不使用反射更好。因为我现在在使用 Kotlin 和 @Value 构造函数时遇到了问题,因为我的团队惯用 @InjectMocks。但还是感谢分享这个答案。 - jmsalcido
1
这是最佳答案,因为它不仅满足了您的需求,而且强制您使用最佳实践进行编程。 - Bruno Miguel
1
这是最简单的可行解决方案。 - SGuru

1

另一种方法是使用@SpringBootTest注释的properties字段。

在这里,我们覆盖了example.firstProperty属性:

@SpringBootTest(properties = { "example.firstProperty=annotation" })
public class SpringBootPropertySourceResolverIntegrationTest {

    @Autowired private PropertySourceResolver propertySourceResolver;

    @Test
    public void shouldSpringBootTestAnnotation_overridePropertyValues() {
        String firstProperty = propertySourceResolver.getFirstProperty();
        String secondProperty = propertySourceResolver.getSecondProperty();

        Assert.assertEquals("annotation", firstProperty);
        Assert.assertEquals("defaultSecond", secondProperty);
    }
}

正如您所看到的,它只覆盖了一个属性。在@SpringBootTest中未提及的属性保持不变。因此,当我们需要仅针对测试覆盖特定属性时,这是一个很好的解决方案。

对于单个属性,您可以直接写出它而不使用大括号:

@SpringBootTest(properties = "example.firstProperty=annotation")

来自回答:https://www.baeldung.com/spring-tests-override-properties#springBootTest

我还鼓励你尽可能地在构造函数中将属性作为参数传递,就像 Dherik 的回答(https://dev59.com/vmAg5IYBdhLWcg3w0Nkz#52955459)一样,因为这样可以轻松地在单元测试中模拟属性。

然而,在集成测试中,通常不会手动创建对象,而是:

  • 使用 @Autowired
  • 您需要修改在间接用于集成测试的类中使用的属性,因为它是某些直接使用的类的深度依赖项。

那么,@SpringBootTest 的解决方案可能会有所帮助。


1
每当可能时,我将字段可见性设置为包保护,以便可以从测试类中访问。我使用Guava的@VisibleForTesting注释对此进行记录(以防下一个人想知道为什么它不是私有的)。这样,我就不必依赖于字段的字符串名称,一切都保持类型安全。
我知道这违反了我们在学校中学到的标准封装实践。但只要团队达成一致,我发现这是最实用的解决方案。

0
如果什么都不起作用,那就试试JAVA反射API,我们将获得目标类的实例,然后使私有字段可访问,然后给它赋值。
以下是需要属性值的UserServiceImpl。
public class UserServiceImpl{

    @Value("${service.username}")
    private String username;

}
下面我们将看到测试类:
@SpringBootTest
@ContextConfiguration
public class PermissionServiceTests {

  @InjectMocks
    private UserServiceImpl userServiceImpl;
  
  @Test
  public void testLogin() {
      Field field = userServiceImpl.getClass().getDeclaredField("username");
      field.setAccessible(true);
      field.set(userServiceImpl, "ajaydatla");
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接