【问题标题】:Mockito mock Java @Value with spring bootMockito 使用 Spring Boot 模拟 Java @Value
【发布时间】:2021-08-02 14:00:46
【问题描述】:

您好,我的 Spring Boot 项目有这个简单的代码:

@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();
    }
}

我想要创建一个简单的 Mockito 测试来检查 @Value 字符串,如下所示:


@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();
        }
    }
}

但我找不到简单的方法来检查我注入的值是否符合我的预期。

有什么想法吗?

【问题讨论】:

    标签: java spring spring-boot unit-testing mockito


    【解决方案1】:

    不幸的是,Spring 的@Value 没有模拟机制。但是,您可以使用ReflectionUtils 的简单解决方法,根据 JavaDoc 用于此目的:

    ReflectionTestUtils 是一组基于反射的实用方法,用于单元和集成测试场景。

    在测试涉及的代码时,通常能够设置非公共字段、调用非公共 setter 方法或调用非公共配置或生命周期回调方法是有益的

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

    ReflectionTestUtils#setField(Object, String, Object) 的 JavaDoc。

    【讨论】:

    • 您好 Nikolas,据我了解,使用 ReflectionTestUtils 是一种不好的做法。
    • 反射和 OOP 原则不是朋友,我知道。使用ReflectionTestUtils 是设置@Value 属性的常见做法。或者,您可能希望创建一个全参数构造函数来传递模拟和字符串属性。
    【解决方案2】:

    iduserName 字段添加getter 方法,而不是模拟Environment 类。

    @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;
        }
    }
    

    在嘲笑时:

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

    另外,您可以使用TestPropertySource 注解来提供完全不同的属性文件:

    @SpringBootTest
    @TestPropertySource(locations = "/application2.properties")
    public class TestClassTest {
    
        @Autowired
        TestClass testClass;
    
        @Test
        public void test() {
            assertEquals("id", testClass.getId());
        }
    }
    

    【讨论】:

    • userRowMapper正在测试中,不是mock,Mockito.when(userRowMapper.getId()).thenReturn("id");会抛出异常
    • 您的方法的问题是,当您使用@SpringBootTest 注解时,它不再是单元测试。
    【解决方案3】:

    我宁愿建议你不要在消费者类上使用内联 @Value 注释。如您所见,类的可测试性降低了。

    您只需创建一个@Configuration bean 并将其注入UserRowMapper 类即可解决您的问题。通过这种方式,您可以使用 DI 轻松模拟测试中的配置。

    请看下面一个简单的实现。

    @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();
            }
        }
    }
    

    【讨论】:

    • 我喜欢这个想法,问题是当你添加一个配置类时,很难理解UserRowMapper 中可以使用哪些属性而不更改类,但这可能是最好的操作但它并没有解决检查用户是否错误地写入了 Value 参数的问题
    • @AndreyKishtov 我没明白你的意思 :-)
    • 我的意思是,当您将代码从 UserRowMapper 移动到 UserMapperConfiguration 时,将更难发现错误,我的主要目标是查看是否有人在 @Value("${bug.value }") 现在甚至看不到错误
    • 其实我写的UserRowMapperConfiguration类不包含任何逻辑,它只是UserRowMapper类可以使用的所有配置值的容器。
    猜你喜欢
    • 1970-01-01
    • 2019-06-16
    • 1970-01-01
    • 2022-01-25
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 2021-05-28
    • 1970-01-01
    相关资源
    最近更新 更多