3

How can I use @Value spring injected properties in a junit @Rule definition?

Background: I want create a junit test with an embedded inmemory sftp server, using FakeSftpServerRule.

Problem: a @Rule is executed before @Value field gets injected.

@Value("${ftp.port}")
private Integer port;

@Value("${ftp.user}")
private String user;

@Value("${ftp.pass}")
private String pass;

@Rule
public final FakeSftpServerRule sftpServer = new FakeSftpServerRule().setPort(port).addUser(user, pass);
membersound
  • 81,582
  • 193
  • 585
  • 1,120
  • Are you able to tweak the `FakeSftpServerRule`? If so you might utilize a `RuleChain` as outlined in this [answer](https://stackoverflow.com/questions/31575238/what-does-rule-do/31575792#31575792) . A `SpringContextRule` as suggested in this answer may look something like [this](https://github.com/mannewolff/java/blob/master/SpringMVC/src/main/java/de/neusta/framework/rules/SpringContextRule.java). Instead of injecting values by Spring you probably need to look up the value from the `Environment` directly – Roman Vottner Jun 19 '19 at 12:16
  • I'm not in control of the rule, as it's coming from the following package: https://github.com/stefanbirkner/fake-sftp-server-rule/blob/master/src/main/java/com/github/stefanbirkner/fakesftpserver/rule/FakeSftpServerRule.java – membersound Jun 19 '19 at 13:05

2 Answers2

1

One solution to tackle your problem is to initialize the Spring context and perform the lookup of the properties yourself, as indicated in my initial comment and by referencing to a sample that ilustrated that concept.

The issue here now is, if you don't take care you might end up initializing the Spring context twice, once with the static definition and once with the regular Spring test setup.

Luckily though, Spring itself comes with an AbstractJUnit4SpringContextTests support class that allows you to reuse an already loaded Spring ApplicationContext.

We can leverage this functionality now to initialize the fake SFTP server but also allow to inject beans into the testcode without the need for manual lookup via context.getBean(...) invocations.

The code below should give you an idea how this can be done:

@ContextConfiguration(classes = SpringTest.Config.class)
public class SpringTest extends AbstractJUnit4SpringContextTests {

  @Configuration
  @PropertySource("classpath:some.properties")
  static class Config {
    @Bean(name = "namedBean")
    String someBean() {
      return "Test";
    }

    @Bean
    UUID uuidGenerator() {
      return UUID.randomUUID();
    }
  }

  static ApplicationContext context;
  static int port;
  static String user;
  static String pass;

  static {
    context = new AnnotationConfigApplicationContext(SpringTest.Config.class);
    Environment env = context.getBean(Environment.class);
    String sPort = env.getProperty("port");
    port = Integer.parseInt(sPort);
    user = env.getProperty("user");
    pass = env.getProperty("pass");
  }

  @Autowired
  private UUID uuidGenerator;

  public SpringTest() {
    // reuse the already initialized Spring application context
    setApplicationContext(context);
  }

  @Rule
  public final FakeSftpServerRule sftpServer = 
          new FakeSftpServerRule().setPort(port).addUser(user, pass);

  @Test
  public void test() {
    String someBean = context.getBean("namedBean", String.class);
    System.out.println(someBean);

    System.out.println(uuidGenearator.toString());
    System.out.println(sftpServer.getPort());
  }
}

A probably more elegant way would be to define something like this on the Spring configuration:

@Configuration
@PropertySource("classpath:some.properties")
@EnableConfigurationProperties(SftpSettings.class)
static class Config {
  ...
}

where SftpSettings is a simple bean class such as

@Validated
@Getter
@Setter
@ConfigurationProperties(prefix="sftp")
public class SftpSettings {
  @NotNull
  private int port;
  @NotNull
  private String user;
  @NotNull
  private String pass;
}

and then perform the lookup on the SftpSettings instead of the Environment:

static {
  context = new AnnotationConfigApplicationContext(SpringTest.Config.class);
  SftpSettings settings = context.getBean(SftpSettings.class);
  port = settings.getPort();
  user = settings.getUser();
  pass = settings.getPass();
}

This way Spring will take care of looking up the values from the property file and on converting these values to the appropriate formats.

Roman Vottner
  • 12,213
  • 5
  • 46
  • 63
0

You can use the @Rule annotation on a method, which is executed after value injection. At least it seems to be the case of SpringJUnit4ClassRunner.

@Value("${ftp.port}")
private Integer port;

@Value("${ftp.user}")
private String user;

@Value("${ftp.pass}")
private String pass;

@Rule
public FakeSftpServerRule getFakeSftpServerRule() {
    return new FakeSftpServerRule().setPort(port).addUser(user, pass);
}

Note if your rule implements both TestRule and MethodRule interfaces, the method may be picked up, and executed twice, which can lead to some issues. To avoid that, change the method's return type to TestRule.

@Rule
public TestRule getFakeSftpServerRule() {
    return new FakeSftpServerRule().setPort(port).addUser(user, pass);
}
Václav Kužel
  • 1,070
  • 13
  • 16