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
{
}
}