Injecting @Autowired private field during testing

后端 未结 6 1009
爱一瞬间的悲伤
爱一瞬间的悲伤 2020-11-29 17:08

I have a component setup that is essentially a launcher for an application. It is configured like so:

@Component
public class MyLauncher {
    @Autowired
            


        
相关标签:
6条回答
  • 2020-11-29 17:20

    Look at this link

    Then write your test case as

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({"/applicationContext.xml"})
    public class MyLauncherTest{
    
    @Resource
    private MyLauncher myLauncher ;
    
       @Test
       public void someTest() {
           //test code
       }
    }
    
    0 讨论(0)
  • 2020-11-29 17:22

    I'm a new user for Spring. I found a different solution for this. Using reflection and making public necessary fields and assign mock objects.

    This is my auth controller and it has some Autowired private properties.

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

    And this is my Junit test for AuthController. I'm making public needed private properties and assign mock objects to them and rock :)

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

    I don't know advantages and disadvantages for this way but this is working. This technic has a little bit more code but these codes can be seperated by different methods etc. There are more good answers for this question but I want to point to different solution. Sorry for my bad english. Have a good java to everybody :)

    0 讨论(0)
  • 2020-11-29 17:25

    You can absolutely inject mocks on MyLauncher in your test. I am sure if you show what mocking framework you are using someone would be quick to provide an answer. With mockito I would look into using @RunWith(MockitoJUnitRunner.class) and using annotations for myLauncher. It would look something like what is below.

    @RunWith(MockitoJUnitRunner.class)
    public class MyLauncherTest
        @InjectMocks
        private MyLauncher myLauncher = new MyLauncher();
    
        @Mock
        private MyService myService;
    
        @Test
        public void someTest() {
    
        }
    }
    
    0 讨论(0)
  • 2020-11-29 17:34

    Sometimes you can refactor your @Component to use constructor or setter based injection to setup your testcase (you can and still rely on @Autowired). Now, you can create your test entirely without a mocking framework by implementing test stubs instead (e.g. Martin Fowler's 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() {
    
        }
    }
    

    This technique especially useful if the test and the class under test is located in the same package because then you can use the default, package-private access modifier to prevent other classes from accessing it. Note that you can still have your production code in src/main/java but your tests in src/main/test directories.


    If you like Mockito then you will appreciate the MockitoJUnitRunner. It allows you to do "magic" things like @Manuel showed you:

    @RunWith(MockitoJUnitRunner.class)
    public class MyLauncherTest
        @InjectMocks
        private MyLauncher myLauncher; // no need to call the constructor
    
        @Mock
        private MyService myService;
    
        @Test
        public void someTest() {
    
        }
    }
    

    Alternatively, you can use the default JUnit runner and call the MockitoAnnotations.initMocks() in a setUp() method to let Mockito initialize the annotated values. You can find more information in the javadoc of @InitMocks and in a blog post that I have written.

    0 讨论(0)
  • 2020-11-29 17:40

    I believe in order to have auto-wiring work on your MyLauncher class (for myService), you will need to let Spring initialize it instead of calling the constructor, by auto-wiring myLauncher. Once that is being auto-wired (and myService is also getting auto-wired), Spring (1.4.0 and up) provides a @MockBean annotation you can put in your test. This will replace a matching single beans in context with a mock of that type. You can then further define what mocking you want, in a @Before method.

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

    Your MyLauncher class can then remain unmodified, and your MyService bean will be a mock whose methods return values as you defined:

    @Component
    public class MyLauncher {
        @Autowired
        MyService myService;
    
        public void doSomething() {
            myService.someVoidMethod();
            myService.someMethodThatCallsSomeStringMethod();
        }
    
        //other methods
    }
    

    A couple advantages of this over other methods mentioned is that:

    1. You don't need to manually inject myService.
    2. You don't need use the Mockito runner or rules.
    0 讨论(0)
  • 2020-11-29 17:41

    The accepted answer (use MockitoJUnitRunner and @InjectMocks) is great. But if you want something a little more lightweight (no special JUnit runner), and less "magical" (more transparent) especially for occasional use, you could just set the private fields directly using introspection.

    If you use Spring, you already have a utility class for this : org.springframework.test.util.ReflectionTestUtils

    The use is quite straightforward :

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

    The first argument is your target bean, the second is the name of the (usually private) field, and the last is the value to inject.

    If you don't use Spring, it is quite trivial to implement such a utility method. Here is the code I used before I found this Spring class :

    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);
            }
        }
    
    0 讨论(0)
提交回复
热议问题