如何在ViewModel构造函数逻辑执行前注入Mock(不修改类结构)
这确实是个棘手的问题——尤其是当你不能碰现有ViewModel的代码时,要在构造函数的逻辑(包括RxJava2的身份验证检查)跑起来之前注入Mock,得用点「黑魔法」才行。下面是几个可行的方案,按场景分:
方案1:反射替换静态/单例依赖
如果ViewModel里的身份验证服务是通过静态单例获取的(比如AuthService.getInstance()),你可以在实例化ViewModel之前,用反射直接替换单例的实例为Mock对象:
@Test public void testViewModelWithStaticDependency() { // 1. 创建Mock的AuthService AuthService mockAuthService = Mockito.mock(AuthService.class); // 模拟auth检查的返回结果(适配RxJava2的Observable/Flowable) Mockito.when(mockAuthService.performAuthCheck()) .thenReturn(Observable.just(true)); try { // 2. 反射获取AuthService的静态单例字段 Field instanceField = AuthService.class.getDeclaredField("instance"); instanceField.setAccessible(true); // 3. 替换为Mock对象 instanceField.set(null, mockAuthService); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); fail("反射替换单例失败"); } // 4. 实例化ViewModel,此时构造函数拿到的是Mock的AuthService ViewModel viewModel = new ViewModel(); // 后续验证逻辑,比如检查auth结果是否正确处理 // ... }
适用场景:依赖是静态单例模式,且你知道单例字段的名称。不需要额外的测试框架(除了Mockito),但依赖于目标类的内部实现细节。
方案2:用PowerMock拦截构造函数内的依赖实例化
如果ViewModel的构造函数里是直接new出AuthService实例的(比如private AuthService authService = new AuthService();),可以用PowerMock拦截这个new操作,提前返回Mock对象:
首先确保测试类配置了PowerMock的Runner和PrepareForTest注解:
@RunWith(PowerMockRunner.class) // 要拦截的类:ViewModel(因为它里面new了AuthService)和AuthService本身 @PrepareForTest({ViewModel.class, AuthService.class}) public class ViewModelTest { @Before public void setupRxJava() { // 配置RxJava2的测试调度器,避免异步逻辑导致测试不稳定 TestScheduler testScheduler = new TestScheduler(); RxJavaPlugins.setIoSchedulerHandler(scheduler -> testScheduler); RxJavaPlugins.setComputationSchedulerHandler(scheduler -> testScheduler); RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> testScheduler); } @After public void teardownRxJava() { // 重置RxJava的调度器,避免影响其他测试 RxJavaPlugins.reset(); RxAndroidPlugins.reset(); } @Test public void testViewModelWithNewDependency() { // 1. 创建Mock的AuthService AuthService mockAuthService = Mockito.mock(AuthService.class); Mockito.when(mockAuthService.performAuthCheck()) .thenReturn(Observable.just(true)); // 2. 拦截ViewModel构造函数内的new AuthService()调用,返回Mock PowerMockito.whenNew(AuthService.class) .withNoArguments() .thenReturn(mockAuthService); // 3. 实例化ViewModel,构造函数会使用Mock的AuthService ViewModel viewModel = new ViewModel(); // 触发RxJava的异步任务(因为用了TestScheduler) TestScheduler testScheduler = (TestScheduler) RxJavaPlugins.getIoSchedulerHandler().apply(Schedulers.io()); testScheduler.triggerActions(); // 4. 验证逻辑,比如检查auth方法是否被调用 Mockito.verify(mockAuthService, Mockito.times(1)).performAuthCheck(); } }
适用场景:依赖是在ViewModel构造函数内直接new出来的。需要引入PowerMock框架,但完全不需要修改原ViewModel代码。
方案3:利用依赖注入框架的测试组件替换
如果你的项目本身使用了Dagger/Hilt这类依赖注入框架,且ViewModel的依赖是通过DI注入的(即使构造函数看起来无参,可能是通过字段注入),可以配置测试专用的DI组件,将AuthService替换为Mock:
比如用Dagger的话,你可以创建一个TestAppComponent,提供Mock的AuthService:
@Singleton @Component(modules = TestAuthModule.class) public interface TestAppComponent extends AppComponent { void inject(ViewModel viewModel); } @Module public class TestAuthModule { @Provides @Singleton public AuthService provideAuthService() { AuthService mockAuthService = Mockito.mock(AuthService.class); Mockito.when(mockAuthService.performAuthCheck()) .thenReturn(Observable.just(true)); return mockAuthService; } }
然后在测试中替换App的组件为TestAppComponent,再实例化ViewModel:
@Test public void testViewModelWithDI() { // 1. 初始化测试组件 TestAppComponent testComponent = DaggerTestAppComponent.create(); // 2. 替换App的全局组件 MyApp app = (MyApp) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext(); app.setAppComponent(testComponent); // 3. 实例化ViewModel,此时DI会注入Mock的AuthService ViewModel viewModel = new ViewModel(); // 触发RxJava任务并验证 // ... }
适用场景:项目本身使用了DI框架,这是最优雅的方案,不依赖反射或字节码修改,且符合DI的设计思想。
内容的提问来源于stack exchange,提问作者nyarian

