Android/测试/单元测试/注入静态方法
外观
您不能通过继承重新定义静态方法,但您通常希望为静态方法(包括“new”运算符)提供测试实现。
像 PowerMock 这样的工具确实允许重新定义静态方法,但如果您不习惯使用它,您仍然可以解决这个问题:如果您使用依赖注入,您可以编写简单的类来包装静态方法并用很少的侵入性代码替换它们。如果您没有使用真正的 DI(例如,由于启动时间),以下内容可能对您有所帮助。
如果您决定坚持使用这种手动 DI,您可能会发现某些方法(尤其是 Android 方法)在不同类的测试中经常出现,以至于值得创建一个外部非静态实用程序类来注入。会有一种诱惑,让它发展成为一个包含您曾经需要的所有静态方法的万用包类(这可能不是问题,除了它是一个庞大的混乱,并且可能掩盖了被测试的类做得太多),但您应该防止偷偷潜入严格来说不是静态方法的东西“因为它是一个方便的单例,可以隐藏一些状态”(这将是错误的)。
如果您是“单一职责”的粉丝,您会看到这个 StaticInjection 实际上应该分成不同的部分——这是正确全面的 DI(尽管是静态 DI,而不是 Guice 等的动态风格)的开端。
添加一个内部类允许您在生产环境中提供默认行为,但在测试中提供特殊行为。该示例表明,不仅 Android 喜欢声明静态方法。
/**
* Activity that does something with a Facebook token.
*/
class MyActivity extends Activity {
// Indirect static method calls, including 'new' for complex objects, to this class.
static StaticInjection STATIC_INJECTION = new StaticInjection();
StaticInjection staticInjection;
// Default constructor creates default StaticInjector
public MyActivity() {
this(STATIC_INJECTION);
}
// Non-default constructor for tests wishing to inject other implementations.
// If your tests are package-scoped, don't make this public.
MyActivity(StaticInjection staticInjection) {
this.staticInjection = staticInjection;
}
// Your unit tests can invoke this; you will need Robolectric or similar to do that on the development machine.
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
String fbToken = staticInjection.sessionStoreGetAccessToken(this);
Application app = staticInjection.getApplication(this);
DooDad doodad = staticInjection.newDooDad(this, bundle);
// use these in some way you can test
}
// Add any additional static stuff you need to stub out for your tests here.
public static class StaticInjection {
public String sessionStoreGetAccessToken(Context context) {
return com.facebook.android.SessionStore.getAccessToken(context);
}
public Application getApplication(Activity activity) {
return activity.getApplication();
}
public DooDad newDooDad(Activity activity, Bundle bundle) {
return new DooDad(activity, bundle);
}
}
}
如果您发现 StaticInjection 类增长过多,您可能在 Activity 中做得太多——尝试遵循“单一职责”原则。
测试将要么子类化并覆盖 StaticInjection 的部分,要么使用模拟来约束使用 StaticInjection 的哪些部分。
@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {
@Rule
public JUnitRuleMockery mockery = new ThreadSafeJUnitRuleMockery.WithImposteriser();
MyActivity activity;
@Mock Bundle bundle;
@Mock Application application;
@Mock DooDad dooDad;
@Test
public void _onCreateAccessesFacebookToken() {
MyActivity.StaticInjection injection = mockery.mock(MyActivity.StaticInjection.class);
mockery.checking(new Expectations() {{
oneOf(injection).sessionStoreGetAccessToken(activity);
will(returnValue("NotReallyAToken");
allowing(injection).getApplication(activity);
will(returnValue(application));
oneOf(injection).newDooDad(activity, bundle);
will(returnValue(dooDad));
}});
activity = new MyActivity(injection);
activity.onCreate(bundle);
// no asserts needed here: mockery will check that sessionStoreGetFacebookToken has been called once.
}
}
以上示例依赖于这些简单的类
import org.jmock.integration.junit4.JUnitRuleMockery;
import org.jmock.lib.concurrent.Synchroniser;
import org.jmock.lib.legacy.ClassImposteriser;
/**
* Mock with extra magic stuff, use ThreadSafeJUnitRuleMockery.WithImposteriser,
* or split the classes out and rename the inner one to something sensible.
*/
public class ThreadSafeJUnitRuleMockery extends JUnitRuleMockery
{
private ThreadSafeJUnitRuleMockery()
{
setThreadingPolicy(new Synchroniser());
}
static public class WithImposteriser extends ThreadSafeJUnitRuleMockery
{
public WithImposteriser()
{
super();
setImposteriser(ClassImposteriser.INSTANCE);
}
}
static public class WithoutImposteriser extends ThreadSafeJUnitRuleMockery
{
}
}