'Mockito mock Java @Value with spring boot

Hi I have this simple code for my Spring Boot Project:

@Component
public class UserRowMapper implements RowMapper<User> {
    @Value("${bug.value}")
    private String id;
    @Value("${wrong.value}")
    private String userName;

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return User.builder()
                .id(rs.getInt(id))
                .userName(rs.getString(userName)).build();
    }
}

what I want is to create a simple Mockito Test that will check @Value strings like so:


@ExtendWith(MockitoExtension.class)
class UserRowMapperTest {
    @Mock
    Environment environment;
    @Mock
    ResultSet resultSet;
    @InjectMocks
    UserRowMapper userRowMapper;

    @Test
    void testMapRow() {
        when(environment.getProperty("user.id")).thenReturn("id");
        when(environment.getProperty("user.userName")).thenReturn("userName");
        try {
            final User user = userRowMapper.mapRow(resultSet, anyInt());
            
            // check if its ok
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

But I can't find a simple way to check if the value I injected is what I expect.

any ideas?



Solution 1:[1]

Unfortunately, there is no mocking mechanism for Spring's @Value. However, you can use a simple workaround using ReflectionUtils that serves for this purpose according to the JavaDoc:

ReflectionTestUtils is a collection of reflection-based utility methods for use in unit and integration testing scenarios.

There are often times when it would be beneficial to be able to set a non-public field, invoke a non-public setter method, or invoke a non-public configuration or lifecycle callback method when testing code involving

ReflectionTestUtils.setField(userRowMapper, "id", "my-id-value");
ReflectionTestUtils.setField(userRowMapper, "userName", "my-userName-value");

JavaDoc for ReflectionTestUtils#setField(Object, String, Object).

Solution 2:[2]

Add getter methods for id and userName fields instead of mocking Environment class.

@Component
public class UserRowMapper implements RowMapper<User> {
    @Value("${bug.value}")
    private String id;

    @Value("${wrong.value}")
    private String userName;

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return User.builder()
                .id(rs.getInt(getId()))
                .userName(rs.getString(getUserName())).build();
    }

    public String getId() {
        return id;
    }

    public String getUserName() {
        return userName;
    }
}

While mocking:

Mockito.when(userRowMapper.getId()).thenReturn("id");
Mockito.when(userRowMapper.getUserName()).thenReturn("userName");

Also, you can use TestPropertySource annotation to provide altogether different properties file:

@SpringBootTest
@TestPropertySource(locations = "/application2.properties")
public class TestClassTest {

    @Autowired
    TestClass testClass;

    @Test
    public void test() {
        assertEquals("id", testClass.getId());
    }
}

Solution 3:[3]

I would rather suggest to you to do not use inline @Value annotation on the consumer class. As you have seen, the class testability decreases.

You can solve your problem simply creating a @Configuration bean and injecting it to the UserRowMapper class. In this way, using DI you can easily mock the configuration in your tests.

See below a naïve implementation.

@Configuration
public class UserRowMapperConfiguration {
    @Value("${bug.value}")
    private String id;

    @Value("${wrong.value}")
    private String userName;

    public String getId() {
        return id;
    }

    public String getUserName() {
        return userName;
    }
}
@Component
public class UserRowMapper implements RowMapper<User> {
    
    private UserRowMapperConfiguration configuration;
    
    public UserRowMapper (UserRowMapperConfiguration configuration) {
        this.configuration = configuration;
    }

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        return User.builder()
                .id(rs.getInt(this.configuration.getId()))
                .userName(rs.getString(this.configuration.getUserName())).build();
    }
}
@ExtendWith(MockitoExtension.class)
class UserRowMapperTest {
    @Mock
    UserRowMapperConfiguration configuration;
    @Mock
    ResultSet resultSet;
    @InjectMocks
    UserRowMapper userRowMapper;

    @Test
    void testMapRow() {
        when(configuration.getId()).thenReturn("id");
        when(configuration.getUserName()).thenReturn("userName");
        try {
            final User user = userRowMapper.mapRow(resultSet, anyInt());
            
            // check if its ok
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

Solution 4:[4]

As thepaoloboi suggested use a configuration class to hold all your configs. Now to test that your config is pointing to the right @Value key, you create an integration test by simply loading that object using spring without loading the whole context. That way it'll be as fast as a unit test.

Here's an example:

@ExtendWith(SpringExtension.class)
@Import(UserRowMapperConfiguration.class)
@TestPropertySource(properties = { "user.id=id" , "user.userName=userName"})
class UserRowMapperConfigurationTest {

    @Autowired
    UserRowMapperConfiguration userRowMapperConfiguration;

    @Test
    void test() {
        assertEquals("id",userRowMapperConfiguration.getId());
        assertEquals("userName",userRowMapperConfiguration.getUserName());
    }
}

and Configuration class:

@Configuration
public class UserRowMapperConfiguration {
    @Value("${bug.value}")
    private String id;

    @Value("${wrong.value}")    
    private String userName;

    public String getId() {
        return id;
    }

    public String getUserName() {
        return userName;
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 Sam Deir