在测试期间注入@Autowired 私有字段

Posted

技术标签:

【中文标题】在测试期间注入@Autowired 私有字段【英文标题】:Injecting @Autowired private field during testing 【发布时间】:2013-05-01 20:04:15 【问题描述】:

我有一个组件设置,它本质上是一个应用程序的启动器。它是这样配置的:

@Component
public class MyLauncher 
    @Autowired
    MyService myService;

    //other methods

MyService 使用@Service Spring 注释进行注释,并且自动装配到我的启动器类中,没有任何问题。

我想为 MyLauncher 编写一些 jUnit 测试用例,为此我开设了一个这样的课程:

public class MyLauncherTest
    private MyLauncher myLauncher = new MyLauncher();

    @Test
    public void someTest() 

    

我可以为 MyService 创建一个 Mock 对象并将其注入到我的测试类中的 myLauncher 中吗?我目前在 myLauncher 中没有 getter 或 setter,因为 Spring 正在处理自动装配。如果可能的话,我不想添加 getter 和 setter。我可以告诉测试用例使用@Before init 方法将模拟对象注入到自动装配的变量中吗?

如果我完全错了,请随意说。我还是新手。我的主要目标是只使用一些 Java 代码或注释,将模拟对象放入 @Autowired 变量中,而无需编写 setter 方法或使用 applicationContext-test.xml 文件。我宁愿在 .java 文件中维护测试用例的所有内容,而不必仅为我的测试维护单独的应用程序内容。

我希望将 Mockito 用于模拟对象。在过去,我通过使用org.mockito.Mockito 并使用Mockito.mock(MyClass.class) 创建我的对象来完成此操作。

【问题讨论】:

【参考方案1】:

您绝对可以在测试中在 MyLauncher 上注入模拟。我敢肯定,如果您展示您正在使用的模拟框架,某人会很快提供答案。对于 mockito,我会考虑使用 @RunWith(MockitoJUnitRunner.class) 并为 myLauncher 使用注释。它看起来像下面的内容。

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher = new MyLauncher();

    @Mock
    private MyService myService;

    @Test
    public void someTest() 

    

【讨论】:

@jpganz18 和你通常使用的 junit,就像在每个方法之前调用 MockitoAnnotations.initMocks 一样 现在,我正在使用这个跑步者:@RunWith(SpringRunner.class) 我可以替换 MockitoJUnitRunner 吗?如果没有,有没有办法在没有它的情况下注入 Mocks? (我并没有真正使用模拟对象。我想注入应用程序使用的相同 Bean。)【参考方案2】:

接受的答案(使用MockitoJUnitRunner@InjectMocks)很棒。但是,如果您想要更轻量级的东西(没有特殊的 JUnit 运行器),并且不那么“神奇”(更透明),特别是偶尔使用,您可以直接使用自省设置私有字段。

如果你使用 Spring,你已经有一个实用类:org.springframework.test.util.ReflectionTestUtils

使用非常简单:

ReflectionTestUtils.setField(myLauncher, "myService", myService);

第一个参数是你的目标 bean,第二个是(通常是私有的)字段的名称,最后一个是要注入的值。

如果你不使用 Spring,实现这样的实用方法是很简单的。这是我在找到这个 Spring 类之前使用的代码:

public static void setPrivateField(Object target, String fieldName, Object value)
        try
            Field privateField = target.getClass().getDeclaredField(fieldName);
            privateField.setAccessible(true);
            privateField.set(target, value);
        catch(Exception e)
            throw new RuntimeException(e);
        
    

【讨论】:

很好的答案!我必须在我的 pom.xml 中包含 mvnrepository.com/artifact/org.springframework/spring-test 才能获得 ReflectionTestUtils 类。 我会说这是一个糟糕的建议。至少有两个原因:1. 任何需要反射的东西通常闻起来很糟糕,并且指向架构问题 2. 依赖注入的全部意义在于能够轻松替换注入的对象,所以再次使用反射你错过了这一点跨度> @artkoshelev 我同意,出于测试目的和“架构”问题,construcotr injectionon 更干净,值得推荐。它还与 java config 一起玩得更好。但是,如果您想测试一些使用字段注入并且不能或不会修改它们的现有 bean,我认为最好在设置中编写一个使用反射的测试,而不是根本没有测试......此外如果反射始终是代码气味或架构问题,那么 Spring 是代码气味,Hibernate 是代码气味,等等...... “除了反射总是代码气味或架构问题之外,Spring 是代码气味,Hibernate 是代码气味,等等......”我当然不是在说好 -经过测试和知名的框架,但实际的应用程序代码是开发人员编写的。 我更喜欢这个答案。【参考方案3】:

有时您可以重构您的@Component 以使用基于构造函数或setter 的注入来设置您的测试用例(您可以并且仍然依赖@Autowired)。现在,您可以通过实现测试存根(例如 Martin Fowler 的 MailServiceStub)来完全不使用模拟框架来创建测试:

@Component
public class MyLauncher 

    private MyService myService;

    @Autowired
    MyLauncher(MyService myService) 
        this.myService = myService;
    

    // other methods


public class MyServiceStub implements MyService 
    // ...


public class MyLauncherTest
    private MyLauncher myLauncher;
    private MyServiceStub myServiceStub;

    @Before
    public void setUp() 
        myServiceStub = new MyServiceStub();
        myLauncher = new MyLauncher(myServiceStub);
    

    @Test
    public void someTest() 

    

如果测试和被测类位于同一个包中,此技术特别有用,因为您可以使用默认的package-private 访问修饰符来防止其他类访问它。请注意,您仍然可以将生产代码放在 src/main/java 中,但将测试放在 src/main/test 目录中。


如果您喜欢 Mockito,那么您将欣赏 MockitoJUnitRunner。它允许您执行 @Manuel 向您展示的“神奇”事情:

@RunWith(MockitoJUnitRunner.class)
public class MyLauncherTest
    @InjectMocks
    private MyLauncher myLauncher; // no need to call the constructor

    @Mock
    private MyService myService;

    @Test
    public void someTest() 

    

或者,您可以使用默认的 JUnit 运行器并在 setUp() 方法中调用 MockitoAnnotations.initMocks() 以让 Mockito 初始化带注释的值。您可以在 @InjectMocks 的 javadoc 和我写的 blog post 中找到更多信息。

【讨论】:

很好的答案,构造函数注入是首选方式,并且与 Kotlin 配合得很好。【参考方案4】:

我是 Spring 的新用户。我为此找到了不同的解决方案。使用反射并公开必要的字段并分配模拟对象。

这是我的身份验证控制器,它有一些自动装配的私有属性。

@RestController
public class AuthController 

    @Autowired
    private UsersDAOInterface usersDao;

    @Autowired
    private TokensDAOInterface tokensDao;

    @RequestMapping(path = "/auth/getToken", method = RequestMethod.POST)
    public @ResponseBody Object getToken(@RequestParam String username,
            @RequestParam String password) 
        User user = usersDao.getLoginUser(username, password);

        if (user == null)
            return new ErrorResult("Kullanıcıadı veya şifre hatalı");

        Token token = new Token();
        token.setTokenId("aergaerg");
        token.setUserId(1);
        token.setInsertDatetime(new Date());
        return token;
    

这是我对 AuthController 的 Junit 测试。我正在公开需要的私有属性并将模拟对象分配给它们并摇滚:)

public class AuthControllerTest 

    @Test
    public void getToken() 
        try 
            UsersDAO mockUsersDao = mock(UsersDAO.class);
            TokensDAO mockTokensDao = mock(TokensDAO.class);

            User dummyUser = new User();
            dummyUser.setId(10);
            dummyUser.setUsername("nixarsoft");
            dummyUser.setTopId(0);

            when(mockUsersDao.getLoginUser(Matchers.anyString(), Matchers.anyString())) //
                    .thenReturn(dummyUser);

            AuthController ctrl = new AuthController();

            Field usersDaoField = ctrl.getClass().getDeclaredField("usersDao");
            usersDaoField.setAccessible(true);
            usersDaoField.set(ctrl, mockUsersDao);

            Field tokensDaoField = ctrl.getClass().getDeclaredField("tokensDao");
            tokensDaoField.setAccessible(true);
            tokensDaoField.set(ctrl, mockTokensDao);

            Token t = (Token) ctrl.getToken("test", "aergaeg");

            Assert.assertNotNull(t);

         catch (Exception ex) 
            System.out.println(ex);
        
    


我不知道这种方式的优点和缺点,但这是有效的。这个技术有更多的代码,但这些代码可以通过不同的方法等分开。这个问题有更多好的答案,但我想指出不同的解决方案。对不起,我的英语不好。祝大家有个好的 java :)

【讨论】:

我对 Spring 也比较陌生。使用@Autowired 似乎使测试变得非常简单。但是,我也不确定这是否是“正确”的解决方案。 问题是关于注射【参考方案5】:

我相信为了在您的 MyLauncher 类(用于 myService)上进行自动装配工作,您需要让 Spring 通过自动装配 myLauncher 来初始化它而不是调用构造函数。一旦它被自动连接(并且 myService 也被自动连接),Spring(1.4.0 及更高版本)提供了一个 @MockBean 注释,您可以将其放入您的测试中。这将用该类型的模拟替换上下文中匹配的单个 bean。然后,您可以在 @Before 方法中进一步定义您想要的模拟。

public class MyLauncherTest
    @MockBean
    private MyService myService;

    @Autowired
    private MyLauncher myLauncher;

    @Before
    private void setupMockBean() 
        doNothing().when(myService).someVoidMethod();
        doReturn("Some Value").when(myService).someStringMethod();
    

    @Test
    public void someTest() 
        myLauncher.doSomething();
    

然后您的 MyLauncher 类可以保持不变,并且您的 MyService bean 将是一个模拟,其方法返回您定义的值:

@Component
public class MyLauncher 
    @Autowired
    MyService myService;

    public void doSomething() 
        myService.someVoidMethod();
        myService.someMethodThatCallsSomeStringMethod();
    

    //other methods

与提到的其他方法相比,此方法的几个优点是:

    您无需手动注入 myService。 您不需要使用 Mockito 跑步者或规则。

【讨论】:

不需要@MockBean 需要@RunWith(SpringRunner.class)吗?【参考方案6】:

看看这个link

然后把你的测试用例写成

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/applicationContext.xml")
public class MyLauncherTest

@Resource
private MyLauncher myLauncher ;

   @Test
   public void someTest() 
       //test code
   

【讨论】:

这是加载Spring Context的正确方法!!但我认为你最好为你的测试创建一个测试上下文.. 这不一定是正确的方法,这完全取决于您要完成的工作。如果您在应用程序上下文中设置休眠会话怎么办?你真的想用单元测试来打一个真正的数据库吗?有些人可能会说“是”,但没有意识到他们正在创建集成测试并且可能会弄乱他们的数据。另一方面,如果您创建了一个测试应用上下文并将 hibernate 指向嵌入式数据库,那么您的数据会更好,但您仍然在创建集成测试,而不是单元测试。 这是所谓的“集成测试”,当你想测试你的组件和整个上下文时。它当然很有用,但与要单独测试单个类的单元测试不同。

以上是关于在测试期间注入@Autowired 私有字段的主要内容,如果未能解决你的问题,请参考以下文章

对普通bean进行Autowired字段注入

对普通bean进行Autowired字段注入

Spring @Autowired 字段 - 哪个访问修饰符,私有或包私有?

@Autowired idea 警告

idea 取消@Autowired 不建议字段注入的警告

@Autowired&@Resource