在测试过程中注入@Autowired私有字段


78

我有一个组件安装程序,它实际上是一个应用程序的启动器。它的配置如下:

@Component
public class MyLauncher {
    @Autowired
    MyService myService;

    //other methods
}

MyService带有@ServiceSpring注释,并自动连接到我的启动器类中,没有任何问题。

我想为MyLauncher编写一些jUnit测试用例,为此,我启动了一个这样的类:

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

    @Test
    public void someTest() {

    }
}

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

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

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

Answers:


84

您可以在测试中绝对在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() {

    }
}

通常使用带有junit的@ jpganz18,就像在每种方法之前调用MockitoAnnotations.initMocks
fd8s0

现在,我正在使用此运行程序:@RunWith(SpringRunner.class)我可以替代MockitoJUnitRunner吗?如果没有,有没有办法注入假人呢?(我并不是真正在使用模拟对象。我想注入应用程序使用的相同的Bean。)
MiguelMunoz

56

可接受的答案(使用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);
        }
    }

1
好答案!我必须在pom.xml中包含mvnrepository.com/artifact/org.springframework/spring-test才能获得ReflectionTestUtils类。
user674669 '18

1
我会说这是一个糟糕的建议。至少有两个原因:1.任何需要反射的东西通常闻起来很臭,并指出体系结构问题2.依赖注入的整个要点是能够轻松替换注入的对象,因此,再次使用反射,您错过了这一点
artkoshelev

@artkoshelev我同意出于测试目的和“建筑”方面的考虑,建议使用construcotr注射剂清洁。与Java config搭配使用也效果更好。但是,如果您想测试一些使用字段注入并且不能或不会修改它们的现有bean,我会认为编写一个在设置中使用反射的测试比根本不使用测试要好。如果反射始终是代码气味或体系结构问题,而不是Spring是代码气味,Hibernate是代码气味,依此类推……
Pierre Henry

“除了反射始终是代码的气味或体系结构问题之外,Spring不仅是代码的气味,Hibernate也是代码的气味,依此类推……等等”我当然不是在谈论经过测试和知名的框架,但是开发人员编写的实际应用程序代码。
artkoshelev

我更喜欢这个答案。
安德烈·雷内(AndreiRînea)'18 -10-10

17

有时,您可以重构@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() {

    }
}

如果测试和被测类位于同一程序包中,则此技术特别有用,因为您可以使用默认的程序包专用访问修饰符来防止其他类访问它。请注意,您仍然可以将生产代码放入其中,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运行器并在方法中调用MockitoAnnotations.initMocks()setUp()以使Mockito初始化带注释的值。您可以在@InjectMocks的javadoc和我撰写的博客文章中找到更多信息。


好的答案,构造函数注入是首选方式,并且与Kotlin一起使用效果很好。
Malkaviano

2

我是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测试。我正在公开所需的私有属性,并将模拟对象分配给它们和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);
        }
    }

}

我不知道这种方式的优缺点,但这是可行的。该技术的代码更多一点,但是这些代码可以用不同的方法等分开。这个问题有更多好的答案,但是我想指出不同的解决方案。对不起,我的英语不好。对所有人都有一个好的Java :)


我对Spring还比较陌生。使用@Autowired似乎可以使测试变得非常简单。我也不确定这是否是“正确的”解决方案。
AnonymousAngelo

问题是关于注射的
MK-rou

2

我相信,要在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
}

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

  1. 您无需手动注入myService。
  2. 您不需要使用Mockito运行器或规则。

并不@MockBean需要@RunWith(SpringRunner.class)
wonsuc

1

看这个链接

然后将您的测试用例写为

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

@Resource
private MyLauncher myLauncher ;

   @Test
   public void someTest() {
       //test code
   }
}

这是加载Spring Context的正确方法!但我认为最好为测试创建测试环境。
TiarêBalbi

这不一定是正确的方法,这完全取决于您要完成的工作。如果要在应用程序上下文中设置休眠会话怎么办?您是否真的要通过单元测试来访问真实的数据库?有人可能会说“是”,但没有意识到他们正在创建集成测试并可能破坏了数据。另一方面,如果创建了测试应用程序上下文并将休眠状态指向嵌入式数据库,那么您的数据就更好了,但是您仍在创建集成测试,而不是单元测试。
2015年

当您要与整个上下文一起测试组件时,这就是所谓的“集成测试”。它当然很有用,但与要单独测试单个类的单元测试不同。
皮埃尔·亨利
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.