我最近和Dagger一起吃饱了,因为DI的概念完全合情合理。其中一种更好的副产品" DI(正如Jake Wharton在他的一个演讲中提出的那样)更易于测试。
所以现在我基本上使用espresso做一些功能测试,我希望能够向应用程序注入虚拟/模拟数据并让活动显示它们。我猜测,这是DI的最大优势之一,这应该是一个相对简单的问题。出于某种原因,我似乎无法绕过它。任何帮助将非常感激。这是我到目前为止所做的事情(我已经写了一个反映我当前设置的例子):
public class MyActivity
extends MyBaseActivity {
@Inject Navigator _navigator;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyApplication.get(this).inject(this);
// ...
setupViews();
}
private void setupViews() {
myTextView.setText(getMyLabel());
}
public String getMyLabel() {
return _navigator.getSpecialText(); // "Special Text"
}
}
这些是我的匕首模块:
// Navigation Module
@Module(library = true)
public class NavigationModule {
private Navigator _nav;
@Provides
@Singleton
Navigator provideANavigator() {
if (_nav == null) {
_nav = new Navigator();
}
return _nav;
}
}
// App level module
@Module(
includes = { SessionModule.class, NavigationModule.class },
injects = { MyApplication.class,
MyActivity.class,
// ...
})
public class App {
private final Context _appContext;
AppModule(Context appContext) {
_appContext = appContext;
}
// ...
}
在我的Espresso测试中,我试图像这样插入一个模拟模块:
public class MyActivityTest
extends ActivityInstrumentationTestCase2<MyActivity> {
public MyActivityTest() {
super(MyActivity.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
ObjectGraph og = ((MyApplication) getActivity().getApplication()).getObjectGraph().plus(new TestNavigationModule());
og.inject(getActivity());
}
public void test_SeeSpecialText() {
onView(withId(R.id.my_text_view)).check(matches(withText(
"Special Dummy Text)));
}
@Module(includes = NavigationModule.class,
injects = { MyActivityTest.class, MyActivity.class },
overrides = true,
library = true)
static class TestNavigationModule {
@Provides
@Singleton
Navigator provideANavigator() {
return new DummyNavigator(); // that returns "Special Dummy Text"
}
}
}
这根本不起作用。我的espresso测试运行,但TestNavigationModule完全被忽略...... arr ...... :(
我做错了什么?有没有更好的方法来使用Espresso模拟模块。我已经搜索并看过正在使用的Robolectric,Mockito等的例子。但我只想要纯Espresso测试,需要用我的模拟替换模块。我该怎么做?
编辑:
所以我使用@ user3399328方法获得静态测试模块列表定义,检查null然后在我的Application类中添加它。我仍然没有得到我的Test注入版本的课程。我有一种感觉,它可能与匕首测试模块定义有关,而不是我的espresso生命周期。我做出这个假设的原因是我添加了调试语句,并发现静态测试模块在应用程序类中注入时是非空的。你能指点一下我可能做错的方向吗?以下是我的定义的代码片段:
MyApplication的:
@Override
public void onCreate() {
// ...
mObjectGraph = ObjectGraph.create(Modules.list(this));
// ...
}
模块:
public class Modules {
public static List<Object> _testModules = null;
public static Object[] list(MyApplication app) {
// return new Object[]{ new AppModule(app) };
List<Object> modules = new ArrayList<Object>();
modules.add(new AppModule(app));
if (_testModules == null) {
Log.d("No test modules");
} else {
Log.d("Test modules found");
}
if (_testModules != null) {
modules.addAll(_testModules);
}
return modules.toArray();
}
}
我的测试类中修改过的测试模块:
@Module(overrides = true, library = true)
public static class TestNavigationModule {
@Provides
@Singleton
Navigator provideANavigator()() {
Navigator navigator = new Navigator();
navigator.setSpecialText("Dummy Text");
return navigator;
}
}
答案 0 :(得分:8)
你的方法不起作用,因为它只发生一次,而且正如Matt所说,当活动的真实注入代码运行时,它将消除你的特殊对象图注入的任何变量。
有两种方法可以让它发挥作用。
快速方法:在您的活动中创建一个公共静态变量,以便测试可以分配覆盖模块,并且如果实际活动代码不为空(仅在测试中发生),则实际活动代码始终包含此模块。它与我的答案here类似,仅用于您的活动基类而不是应用程序。
更长,可能更好的方法:重构代码,以便所有活动注入(更重要的是图创建)发生在一个类中,类似于ActivityInjectHelper。在您的测试包中,创建另一个名为ActivityInjectHelper的类,其中包含实现相同方法的完全相同的包路径,除了您的测试模块外。因为首先加载测试类,所以应用程序将使用测试ActivityInjectHelper执行。再次,它与我的答案here相似,只是针对另一个班级。
更新:
我看到你发布了更多的代码,它已接近工作,但没有雪茄。对于活动和应用程序,测试模块需要在onCreate()运行之前进入。在处理活动对象图时,在测试的getActivity()之前的任何时候都可以。在处理应用程序时,它有点困难,因为在setUp()运行时已经调用了onCreate()。幸运的是,在测试的构造函数中进行操作 - 此时尚未创建应用程序。我在第一个链接中简要提到了这一点。
答案 1 :(得分:8)
使用Dagger 2和Espresso 2确实有所改善。这就是测试用例现在的样子。请注意,ContributorsModel由Dagger提供。完整演示可在此处获取:https://github.com/pmellaaho/RxApp
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
ContributorsModel mModel;
@Singleton
@Component(modules = MockNetworkModule.class)
public interface MockNetworkComponent extends RxApp.NetworkComponent {
}
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(
MainActivity.class,
true, // initialTouchMode
false); // launchActivity.
@Before
public void setUp() {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
RxApp app = (RxApp) instrumentation.getTargetContext()
.getApplicationContext();
MockNetworkComponent testComponent = DaggerMainActivityTest_MockNetworkComponent.builder()
.mockNetworkModule(new MockNetworkModule())
.build();
app.setComponent(testComponent);
mModel = testComponent.contributorsModel();
}
@Test
public void listWithTwoContributors() {
// GIVEN
List<Contributor> tmpList = new ArrayList<>();
tmpList.add(new Contributor("Jesse", 600));
tmpList.add(new Contributor("Jake", 200));
Observable<List<Contributor>> testObservable = Observable.just(tmpList);
Mockito.when(mModel.getContributors(anyString(), anyString()))
.thenReturn(testObservable);
// WHEN
mActivityRule.launchActivity(new Intent());
onView(withId(R.id.startBtn)).perform(click());
// THEN
onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0))
.check(matches(hasDescendant(withText("Jesse"))));
onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 0))
.check(matches(hasDescendant(withText("600"))));
onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1))
.check(matches(hasDescendant(withText("Jake"))));
onView(ViewMatchers.nthChildOf(withId(R.id.recyclerView), 1))
.check(matches(hasDescendant(withText("200"))));
}
答案 2 :(得分:1)
对getActivity的调用实际上会启动您在此过程中调用onCreate的活动,这意味着您不会及时将测试模块添加到图表中以供使用。使用activityInstrumentationTestcase2,您无法在活动范围内正确注入。我通过使用我的应用程序为我的活动提供依赖关系,然后将模拟对象注入活动将使用的模型对象来解决这个问题。它并不理想,但它有效。您可以使用像Otto这样的事件总线来帮助提供依赖关系。
答案 3 :(得分:0)
编辑:以下帖子http://systemdotrun.blogspot.co.uk/2014/11/android-testing-with-dagger-retrofit.html
使用Espresso + Dagger测试Activity
我已完成以下
受@ user3399328答案的启发我的Application类中有一个DaggerHelper
类,它允许测试用例使用提供模拟的Test @Provider
覆盖@Modules
。只要
1)这是在testCases getActivity()
调用之前完成的(因为我的注入调用发生在Activity.onCreate
内的我的活动中)
2)tearDown从对象图中删除测试模块。
以下示例。
注意:这并不理想,因为这会遇到使用IoC工厂方法的类似缺陷,但至少这种方式只能在tearDown()中进行一次调用,以使被测系统恢复正常。
我的DaggerHelper
班级中的Application
public static class DaggerHelper
{
private static ObjectGraph sObjectGraph;
private static final List<Object> productionModules;
static
{
productionModules = new ArrayList<Object>();
productionModules.add(new DefaultModule());
}
/**
* Init the dagger object graph with production modules
*/
public static void initProductionModules()
{
initWithModules(productionModules);
}
/**
* If passing in test modules make sure to override = true in the @Module annotation
*/
public static void initWithTestModules(Object... testModules)
{
initWithModules(getModulesAsList(testModules));
}
private static void initWithModules(List<Object> modules)
{
sObjectGraph = ObjectGraph.create(modules.toArray());
}
private static List<Object> getModulesAsList(Object... extraModules)
{
List<Object> allModules = new ArrayList<Object>();
allModules.addAll(productionModules);
allModules.addAll(Arrays.asList(extraModules));
return allModules;
}
/**
* Dagger convenience method - will inject the fields of the passed in object
*/
public static void inject(Object object) {
sObjectGraph.inject(object);
}
}
我的测试类中的我的测试模块
@Module (
overrides = true,
injects = ActivityUnderTest.class
)
static class TestDataPersisterModule {
@Provides
@Singleton
DataPersister provideMockDataPersister() {
return new DataPersister(){
@Override
public void persistDose()
{
throw new RuntimeException("Mock DI!"); //just a test to see if being called
}
};
}
}
测试方法
public void testSomething()
{
MyApp.DaggerHelper.initWithTestModules(new TestDataPersisterModule());
getActivity();
...
}
撕下
@Override
public void tearDown() throws Exception
{
super.tearDown();
//reset
MyApp.DaggerHelper.initProductionModules();
}