遵循Java组合(Composition)模式时如何在Mockito中mock依赖类?
概念澄清:组合与依赖注入的差异
你之前的认知存在一个常见误区:组合和依赖注入并不是互斥的设计概念,二者属于完全不同的维度:
- 组合是面向对象中类关系的类型定义,代表强绑定的Has-A(拥有)关系,核心判定规则是从属对象的生命周期和宿主完全绑定:宿主实例销毁时,从属对象也会同步销毁,没有其他外部引用持有该从属对象。它只定义关系的性质,并不限制从属对象的实例化方式。
- 依赖注入是实现依赖管理的编码技巧,核心逻辑是不在类内部硬编码依赖的实例化逻辑,而是通过构造函数、Setter等方式从外部传入依赖实例。它是实现依赖的手段,完全可以和组合搭配使用。
你完全可以通过构造函数传入Foo实例,同时保证Bar和Foo是组合关系:只要Bar不对外暴露Foo的引用,外部也不会单独持有传入的Foo实例,Foo的生命周期就完全和Bar绑定,完全符合组合的定义。
Mockito测试解决方案
这里提供两种方案,优先推荐第一种重构方案,对代码侵入性极低,也不会破坏原有封装和组合特性。
方案1:重构Bar类新增包级私有构造(推荐)
给Bar类新增一个仅对同包下测试类可见的构造函数,用于传入mock的Foo实例,对外仍然保留原有的无参构造,不影响原有业务代码的使用:
public class Bar implements MyRequiredObjectsInterface { private Foo foo; // 对外公开的无参构造,兼容原有使用逻辑,内部自行实例化Foo public Bar() { this(new Foo()); } // 包级私有构造,仅同包下的测试类可调用,用于传入mock的Foo实例 Bar(Foo foo) { this.foo = foo; } @Override public String stringOne() { return foo.stringOne(); } @Override public List<String> listOne() { return foo.listOne(); } }
对应的测试代码如下:
import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; public class BarTest { private Foo foo; private Bar bar; @Before public void setUp() { // mock Foo实例 this.foo = Mockito.mock(Foo.class); // 传入mock实例构造Bar this.bar = new Bar(foo); } @Test public void testListOne() { // 预设mock的返回值 List<String> mockResult = Arrays.asList("a", "b", "c"); Mockito.when(foo.listOne()).thenReturn(mockResult); // 验证Bar的方法调用结果符合预期 assertEquals(mockResult, bar.listOne()); } }
方案2:无需修改生产代码,反射替换私有字段
如果不能修改原有Bar类的代码,可以通过反射强制修改Bar内部的私有foo字段为mock对象:
import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; public class BarTest { private Foo foo; private Bar bar; @Before public void setUp() throws NoSuchFieldException, IllegalAccessException { this.bar = new Bar(); this.foo = Mockito.mock(Foo.class); // 反射获取Bar类的foo字段 Field fooField = Bar.class.getDeclaredField("foo"); // 开放私有字段的访问权限 fooField.setAccessible(true); // 替换Bar内部的foo实例为mock对象 fooField.set(bar, foo); } @Test public void testListOne() { List<String> mockResult = Arrays.asList("a", "b", "c"); Mockito.when(foo.listOne()).thenReturn(mockResult); assertEquals(mockResult, bar.listOne()); } }
该方案的缺点是测试代码和Bar类的内部字段强耦合,一旦Foo字段名修改,测试就会失效,仅作为临时方案使用。
内容的提问来源于stack exchange,提问作者TechGuyChris
相关产品推荐
相关产品推荐

