如何在Android中使用Dagger2依赖注入和Robolectric进行测试?

7

我最近在一款Android应用程序中实现了Dagger2,以便进行轻松的依赖注入,但在这样做之后,我的部分测试已经停止工作。

现在我正在努力理解如何调整我的测试以适用于Dagger2?我正在使用Robolectric来运行我的测试。

以下是我使用Dagger2的方式,我最近才学会,所以这可能是不良习惯,而且并没有帮助测试,因此请指出我可以做出的任何改进。

我有一个AppModule,其内容如下:

@Module
public class MyAppModule {

    //Application reference
    Application mApplication;

    //Set the application value
    public MyAppModule(Application application) {
        mApplication = application;
    }

    //Provide a singleton for injection
    @Provides
    @Singleton
    Application providesApplication() {
        return mApplication;
    }
}

我所说的NetworkModule是提供注入对象的模块,具体如下:

@Module
public class NetworkModule {

private Context mContext;

//Constructor that takes in the required context and shared preferences objects
public NetworkModule(Context context){
    mContext = context;
}

@Provides
@Singleton
SharedPreferences provideSharedPreferences(){
    //...
}

@Provides @Singleton
OkHttpClient provideOKHttpClient(){
    //...
}

@Provides @Singleton
Picasso providePicasso(){
    //...
}

@Provides @Singleton
Gson provideGson(){
    //...
}
}

然后这个组件就是这样的:

Singleton
@Component(modules={MyAppModule.class, NetworkModule.class})
public interface NetworkComponent {

    //Activities that the providers can be injected into
    void inject(MainActivity activity);
    //...
}

在我的测试中,我使用Robolectric,并且我有一个测试变量的应用程序类,如下所示:

public class TestMyApplication extends TestApplication {

    private static TestMyApplication sInstance;
    private NetworkComponent mNetworkComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
        mNetworkComponent = DaggerTestMyApplication_TestNetworkComponent.builder()
                .testMyAppModule(new TestMyAppModule(this))
                .testNetworkModule(new TestNetworkModule(this)).build();
    }

    public static MyApplication getInstance() {
        return sInstance;
    }

    @Override public NetworkComponent getNetComponent() {
        return mNetworkComponent;
    }
}

正如你所看到的,我正在努力确保使用模拟版本的Dagger2 Modules,这些也被模拟为使用模拟的MyAppModule返回TestMyApplication和模拟的NetworkModule返回模拟对象,我还有一个扩展真实NetworkComponent的模拟NetworkComponent。

在测试的设置中,我使用Robolectric创建Activity:

//Build activity using Robolectric
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
activity = controller.get();

controller.create(); //Create out Activity

这将创建Activity并启动onCreate方法,问题就出在这里,onCreate方法中我有以下代码来将Activity注入到组件中,以便它可以像这样使用Dagger2:

@Inject Picasso picasso; //Injected at top of Activity

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
MyApplication.getInstance().getNetComponent().inject(this); 

picasso.load(url).fetch();

这里的问题是,当运行测试时,picasso变量会出现NullPointerException异常,因此我猜测我的Dagger2设置在测试中有遗漏的链接?

编辑:添加TestNetworkModule

@Module
public class TestNetworkModule {

    public TestNetworkModule(Context context){

    }

    @Provides
    @Singleton
    SharedPreferences provideSharedPreferences(){
        return Mockito.mock(SharedPreferences.class);
    }


    @Provides @Singleton
    Gson provideGson(){
        return Mockito.mock(Gson.class);
    }

    @Provides @Singleton
    OkHttpClient provideOKHttpClient(){
        return Mockito.mock(OkHttpClient.class);
    }

    @Provides @Singleton
    Picasso providePicasso(){
        return  Mockito.mock(Picasso.class);
    }

}

你能添加一下在你的TestNetworkModule中的内容吗? - jbarat
当然,我现在刚刚做了。 - Donal Rafferty
你是否定义了当调用你的Picasso Mock时应该发生什么?就像这样:when(picassoMock.load(anyString())).thenReturn(something)。 - jbarat
我还没有实现那个,我需要在每个测试中都这样做吗? - Donal Rafferty
它明确表示不要在单元测试中使用Dagger:https://google.github.io/dagger/testing.html - IgorGanapolsky
显示剩余3条评论
2个回答

13

您无需向 TestApplication 和模块中添加 setter。您正在使用 Dagger 2,因此您应该在测试中使用它来注入依赖项:

首先,在 MyApplication 中创建一个方法来检索 ApplicationComponent。该方法将在 TestMyApplication 类中进行重写:

public class MyApplication extends Application {

    private ApplicationComponent mApplicationComponent;

    public ApplicationComponent getOrCreateApplicationComponent() {
        if (mApplicationComponent == null) {
            mApplicationComponent = DaggerApplicationComponent.builder()
                    .myAppModule(new MyAppModule(this))
                    .networkModule(new NetworkModule())
                    .build();
        }
        return mApplicationComponent;
    }
}

然后创建一个TestNetworkComponent:

@Singleton
@Component(modules = {MyAppModule.class, TestNetworkModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
    void inject(MainActivityTest mainActivityTest);
}
在TestNetworkModule中返回一个模拟对象
@Provides
@Singleton
Picasso providePicasso(){
    return Mockito.mock(Picasso.class);
}
在你的TestMyApplication中,构建TestNetworkComponent:
public class TestMyApplication extends MyApplication {

    private TestApplicationComponent testApplicationComponent;

    @Override
    public TestApplicationComponent getOrCreateApplicationComponent() {
        if (testApplicationComponent == null) {
            testApplicationComponent = DaggerTestApplicationComponent
                    .builder()
                    .myAppModule(new MyAppModule(this))
                    .testNetworkModule(new TestNetworkModule())
                    .build();
        }
        return testApplicationComponent;
    }
}

然后在您的MainActivityTest中使用application标签运行并注入依赖项:

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, application = TestMyApplication.class)
public class MainActivityTest {

    @Inject
    Picasso picasso;

    @Before
    public void setup() {
        ((TestMyApplication)RuntimeEnvironment.application).getOrCreateApplicationComponent().inject(this);
        Mockito.when(picasso.load(Matchers.anyString())).thenReturn(Mockito.mock(RequestCreator.class));
    }


    @Test
    public void test() {
        Robolectric.buildActivity(MainActivity.class).create();
    }

}

你的 Picasso 字段已经注入了你的 Picasso 模拟数据,现在可以与其进行交互。


当我在测试中使用Mockito.when时,以这种方式进行操作会出现错误 - 不能在验证或存根之外使用参数匹配器。 参数匹配器的正确用法示例: when(mock.get(anyInt())).thenReturn(null); doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject()); verify(mock).someMethod(contains("foo"))此外,这个错误可能会出现,因为您使用了无法被模拟的方法的参数匹配器。 以下方法不能被存根/验证:final/private/equals()/hashCode()。 - Donal Rafferty
实际上,在按照你的代码进行操作后,我无法从测试类中进行注入,出现了编译错误,显示为:"error:no suitable method found for inject(MainActivityTest)"。但是我的Dagger创建对象如下所示:DaggerTestMyApplication_TestNetworkComponent。 - Donal Rafferty
我已经完成了我的回答。请看一下 @DonalRafferty - Steve C
1
"应用程序"已被弃用。 - Piotr Badura

1

仅仅返回模拟数据是不够的。您需要指示您的模拟对象在不同的调用中应该返回什么。

我将为您提供一个Picasso模拟的示例,但对于所有模拟都应该类似。 我正在地铁上编写此伪代码。

更改TestMyApplication以便您可以从外部设置模块,例如:

public class TestMyApplication extends TestApplication {

    private static TestMyApplication sInstance;
    private NetworkComponent mNetworkComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
    }

    public void setModules(MyAppModule applicationModule, NetworkModule networkModule) {
        this.applicationModule = applicationModule;
        this.mNetworkComponent = DaggerApplicationComponent.builder()
                .applicationModule(applicationModule)
                .domainModule(networkModule)
                .build();
    }

    public static MyApplication getInstance() {
        return sInstance;
    }

    @Override public NetworkComponent getNetComponent() {
        return mNetworkComponent;
    }
}

现在你可以从测试中控制你的模块。
下一步是使您的模拟数据可访问。可以类似这样实现:
@Module
public class TestNetworkModule {

    private Picasso picassoMock;

    ...

    @Provides @Singleton
    Picasso providePicasso(){
        return picassoMock;
    }

    public void setPicasso(Picasso picasso){
        this.picasso = picasso;
    }
}

现在您可以控制所有的模拟。
现在一切都准备就绪,让我们创建一个测试:
@RunWith(RobolectricGradleTestRunner.class)
public class PicassoTest {

    @Mock Picasso picasso;
    @Mock RequestCreator requestCreator;

    @Before
    public void before(){
        initMocks(this);

        when(picassoMock.load(anyString())).thenReturn(requestCreator);

        TestApplication app = (TestApplication) RuntimeEnvironment.application;

        TestNetworkModule networkModule = new TestNetworkModule(app);
        networkModule.setPicasso(picasso);

        app.setModules(new TestMyAppModule(this), networkModule);
        //Build activity using Robolectric
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        activity = controller.get();
        activity.create();
    }

    @Test
    public void test(){
        //the test
    }

    @Test
    public void test2(){
        //another test
    }
}

现在您可以编写测试了。由于设置在before中,您无需在每个测试中都进行此操作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接