使用Mockito对SharedPreferences包装器进行单元测试遇NPE问题排查
兄弟,你踩的这个坑太常见了——你只Mock了SharedPreferences对象,但忘了Mock它返回的Editor实例!当代码调用sharedPreferences.edit()时,默认Mock的对象会返回null,自然就触发空指针了。下面分两种情况给你解决思路:
一、不用第三方工具:手动Mock接口实现
既然SharedPreferences和Editor都是接口,我们可以自己写一个极简的测试实现类,模拟必要的行为,完全不需要依赖任何框架。
举个例子,你可以写这样的测试实现:
import android.content.SharedPreferences; import java.util.HashMap; import java.util.Map; // 手动Mock的SharedPreferences实现,只实现测试需要的方法 class TestSharedPrefs implements SharedPreferences { private final Map<String, Object> storage = new HashMap<>(); @Override public Editor edit() { // 返回我们自己Mock的Editor return new TestEditor(); } // 按需实现你需要的get方法,比如getString @Override public String getString(String key, String defValue) { return (String) storage.getOrDefault(key, defValue); } // 其他未用到的方法(比如getInt、registerOnSharedPreferenceChangeListener)可以直接抛出UnsupportedOperationException,或者留空 @Override public int getInt(String key, int defValue) { throw new UnsupportedOperationException("Not implemented for test"); } // 内部类实现Editor接口 class TestEditor implements Editor { @Override public Editor putString(String key, String value) { storage.put(key, value); return this; // 链式调用需要返回自身 } @Override public boolean commit() { return true; // 模拟提交成功 } // 同样,按需实现其他put方法,未用到的可以抛出异常 @Override public Editor putInt(String key, int value) { throw new UnsupportedOperationException("Not implemented for test"); } // 其他Editor方法同理... } }
然后在你的测试类里,把包装器依赖的SharedPreferences换成这个TestSharedPrefs实例:
@Test public void testSaveString() { TestSharedPrefs testPrefs = new TestSharedPrefs(); YourPrefsWrapper wrapper = new YourPrefsWrapper(testPrefs); wrapper.save("test_key", "test_value"); // 验证结果是否符合预期 assertEquals("test_value", testPrefs.getString("test_key", null)); }
这样调用edit()时就会返回我们自己实现的TestEditor,不会再出现NPE了。缺点是如果你的包装器用到了很多SharedPreferences的方法,需要写不少模板代码,但胜在完全无依赖。
二、用工具简化:选择Mockito(Android单元测试标配)
如果觉得手动写实现太繁琐,Mockito是最适合的选择——它是Java/Android生态中最流行的Mock框架,能帮你快速创建Mock对象,并且灵活设置方法的返回值和行为。
步骤1:添加依赖
在你的模块build.gradle(Module级别)里添加Mockito的测试依赖:
dependencies { // 单元测试依赖 testImplementation 'org.mockito:mockito-core:5.+' // 如果是Android Instrumented测试,用下面这个 // androidTestImplementation 'org.mockito:mockito-android:5.+' }
步骤2:编写测试代码
用Mockito创建SharedPreferences和Editor的Mock实例,并指定edit()返回Mock的Editor:
import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.mockito.Mockito.*; public class YourPrefsWrapperTest { @Mock private SharedPreferences mockPrefs; @Mock private SharedPreferences.Editor mockEditor; private YourPrefsWrapper wrapper; @Before public void setUp() { // 初始化Mock对象 MockitoAnnotations.openMocks(this); // 关键:让mockPrefs的edit()方法返回我们的mockEditor when(mockPrefs.edit()).thenReturn(mockEditor); // 因为Editor是链式调用,所以要设置put方法返回自身 when(mockEditor.putString(anyString(), anyString())).thenReturn(mockEditor); // 设置commit()返回true(模拟提交成功) when(mockEditor.commit()).thenReturn(true); // 初始化你的包装器 wrapper = new YourPrefsWrapper(mockPrefs); } @Test public void testSaveString() { wrapper.save("test_key", "test_value"); // 验证是否正确调用了putString和commit方法 verify(mockEditor).putString("test_key", "test_value"); verify(mockEditor).commit(); } }
这样一来,当你的包装器调用mockPrefs.edit()时,会返回我们预先Mock好的mockEditor,完全不会出现NPE。而且Mockito还能帮你验证方法的调用情况,让测试更严谨。
总结
- 如果你坚持不用第三方工具,手动实现
SharedPreferences和Editor接口是可行的,适合简单场景; - 但大多数情况下,Mockito能大大提升测试效率,是Android单元测试的标准工具,学习成本也很低,非常值得入手。
内容的提问来源于stack exchange,提问作者RedBassett

