在测试期间注入@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 私有字段的主要内容,如果未能解决你的问题,请参考以下文章