I recently implemented Dagger2 into an Android application for easy dependency injection but after doing so some of my tests have stopped working.
Now I am trying to understand how to adjust my tests to work with Dagger2? I am using Robolectric for running my tests.
Here is how I use Dagger2, I have only recently learned it so this may be bad practice and not helping the tests so please do point out any improvements I can make.
I have an AppModule which is as follows:
#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;
}
}
And what I call a NetworkModule that provides the objects for injection that is as follows:
#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(){
//...
}
}
And then the Component is like this:
Singleton
#Component(modules={MyAppModule.class, NetworkModule.class})
public interface NetworkComponent {
//Activities that the providers can be injected into
void inject(MainActivity activity);
//...
}
For my tests I am using Robolectric, and I have a Test variant of my Application class as follows:
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;
}
}
As you can see I am trying to make sure the mocked versions of my Dagger2 Modules are used, these are mocked as well with the mocked MyAppModule returning the TestMyApplication and the mocked NetworkModule returning mocked objects, I also have a mocked NetworkComponent which extends the real NetworkComponent.
In the setup of a test I create the Activity using Robolectric like this:
//Build activity using Robolectric
ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
activity = controller.get();
controller.create(); //Create out Activity
This creates the Activity and starts the onCreate, and this is where the issue occurs, in the onCreate I have the following piece of code to inject the Activity into the component so it can use Dagger2 like this:
#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();
The problem here is that when running the test I get a NullPointerException on the picasso variable, so I guess my Dagger2 setup has a missing link somewhere for the tests?
EDIT: Adding 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);
}
}
You don't need to add setters to your TestApplication and modules. You are using Dagger 2 so you should use it to inject dependencies in your test too:
First in your MyApplication create a method to retrieve the ApplicationComponent. This method will be overrided in the TestMyApplication class:
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;
}
}
then create a TestNetworkComponent:
#Singleton
#Component(modules = {MyAppModule.class, TestNetworkModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
void inject(MainActivityTest mainActivityTest);
}
In the TestNetworkModule return a mock
#Provides
#Singleton
Picasso providePicasso(){
return Mockito.mock(Picasso.class);
}
In your TestMyApplication, build the 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;
}
}
then in your MainActivityTest run with the application tag and inject your dependency:
#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();
}
}
Your Picasso field has been injected with your Picasso mock now you can interact with it.
Just giving back mocks are not enough. You need to instruct your mocks what they should return for different calls.
I'm giving you an example for just the Picasso mock, but it should be similar for all.
I'm writing this on the Tube, so treat this as pseudo code.
Change your TestMyApplication so you can set the modules from outside something like this:
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;
}
}
Now you can control your modules from the tests.
Next step make your mocks accesable. Something like this:
#Module
public class TestNetworkModule {
private Picasso picassoMock;
...
#Provides #Singleton
Picasso providePicasso(){
return picassoMock;
}
public void setPicasso(Picasso picasso){
this.picasso = picasso;
}
}
Now you can control all your mock.
Now everything is set up for testing lets make one:
#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
}
}
So now you can write your tests. Because the setup is in the before you don't need to do this in every test.
Related
Here is my module:
#Module
#InstallIn(SingletonComponent.class)
public class BleRepositoryModule {
#Provides
public static BleRepository providesPedalRepository(#ApplicationContext Context context) {
return new BleRepository(context);
}
}
This is the class that I want to be injected as a singleton, that is created by the module
#Singleton
public class BleRepository {
#Inject
public BleRepository(#ActivityContext Context context) {
}
}
I am injecting this singleton in viewmodels such as this:
#HiltViewModel
public class PedalViewModel extends AbstractViewModel {
#Inject
public PedalViewModel(BleRepository bleRepository) {
super();
}
}
However, I see the constructor is called every time it is injected, so it's not acting like a singleton
So i've been learning Dagger 2 Android but i am having problem. I want to pass value on runtime. I've read a lot of blogs etc. but couldn't find any solution. Every dagger source is about Dagger 2 not Dagger 2 Android. Is there anyway to do it on Dagger 2 Android too or should i simply move to Dagger 2?
This is AppComponent.java
#Singleton
#Component(
modules = {
AndroidSupportInjectionModule.class,
ActivityBuildersModule.class,
AppModule.class,
}
)
public interface AppComponent extends AndroidInjector<BaseApplication> {
#Component.Builder
interface Builder{
#BindsInstance
Builder application(Application application);
AppComponent build();
}
}
This is ActivityBuildersModule.java
#Module
public abstract class ActivityBuildersModule {
//For injection class
#AuthScope
#ContributesAndroidInjector(
modules = {
AuthModule.class,
InjectFragmentBuildersModule.class, //Only gonna exist on AuthActivity Scope
MainViewModelsModule.class,
})
abstract AuthActivity contributeAuthActivity();
}
This is AuthModule.java
#Module
public class AuthModule {
//SubComponent
#AuthScope
#Provides
static Tiers getTiers(){
return new Tiers();
}
#AuthScope
#Provides
static Engine getEngine(){
return new Engine();
}
#AuthScope
#Provides
static Car getCar(Tiers tiers, Engine engine, #Named("String1")String string){
return new Car(tiers,engine,string);
}
#AuthScope
#Provides
static FragmentManager getFragmentManager(AuthActivity authActivity){
return authActivity.getSupportFragmentManager();
}
}
And lastly AuthActivity.java
public class AuthActivity extends DaggerAppCompatActivity {
private static final String TAG = "AuthActivity";
#Inject
Car car;
#Inject
#Named("String1")
String test1;
#Inject
#Named("String2")
String test2;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auth);
car.startCar();
car.setmBrand("new test brand");
NavController navController= Navigation.findNavController(this,R.id.nav_host_fragment);
navController.navigate(R.id.injectFragment);
}
}
So lets say i want to pass some string to Car instead of #Named("String1") how can i do it? I solved the problem with setters but i can't solve that problem like that everytime.
I am trying to inject Context in non-activity class. In my case, I am trying to inject in Model class (from MVP pattern). I need Context to get string from resources. The method result() returns Observable which returns ViewModel which is modified API model. ViewModel is used to show in MainActivity. I tried this code, but Context is null, I think that it is not injected.
public class MainActivityModel implements MainActivityMVP.Model {
#Inject
Context context;
private Repository repository;
public MainActivityModel(Repository repository) {
((App) getApplication()).getComponent().inject(this); //Cannot resolve getApplication() method :(
this.repository = repository;
}
#Override
public Observable<List<ViewModel>> result(String username) {
return repository.getRepositories(username).map(new Func1<List<Repository>, List<ViewModel>>() {
#Override
public List<ViewModel> call(List<Repository> repositories) {
List<ViewModel> viewModelList = new ArrayList<ViewModel>();
for (Repository repository : repositories) {
// here Context is null
viewModelList.add(new ViewModel(context.getString(R.string.repository) + repository.getName()));
}
return viewModelList;
}
});
}
}
This is Component class, where there is inject(MainActivityModel target) method which I am not able to use inject MainActivityModel because getApplication is not available from non-Activity class:
#Singleton
#Component(modules = {ApplicationModule.class, MainActivityModule.class, GithubApiModule.class})
public interface ApplicationComponent {
void inject(MainActivity target);
void inject(MainActivityModel target);
}
I think that Context can be sent via result(String username, Context context) method. But what is the meaning of Dependency Injection if I passing Context as method parameter? Maybe I misunderstood fundamental concept of DI.
So my question: is it possible to inject Context in non-activity class? Or it should be passed as method parameter?
For injecting a Context you will need to write a module with a provides method:
#Module (injects = {MainActivityModel.class})
public class RootModule {
private Context context;
public RootModule(App application) {
this.context = application.getApplicationContext();
}
#Provides
#Singleton
Context provideContext() {
return context;
}
}
In your custom App class:
public class App extends Application {
public ObjectGraph objectGraph;
#Override
public void onCreate() {
super.onCreate();
objectGraph = ObjectGraph.create(getInjectionModule());
}
protected Object getInjectionModule() {
return new RootModule(this);
}
}
And inject in model's constructor:
public MainActivityModel(Repository repository) {
((App) getApplication()).objectGraph.inject(this);
this.repository = repository;
}
Sorry for my english, now i begin learn dagger2 and i cant understand why i have error:
Error:(9, 10) error:
test.dagger.dagger.modules.MainActivityPresenterModule cannot be
provided without an #Inject constructor or from an #Provides- or
#Produces-annotated method.
test.dagger.dagger.modules.MainActivityPresenterModule is injected at
test.dagger.view.activitys.MainActivity.mainActivityPresenterModule
test.dagger.view.activitys.MainActivity is injected at
test.dagger.dagger.components.AppComponent.injectMainActivity(mainActivity)
App
public class App extends Application {
private static AppComponent component;
#Override
public void onCreate() {
super.onCreate();
component = DaggerAppComponent.create();
}
public static AppComponent getComponent() {
return component;
}
}
AppComponent
#Component(modules = {MainActivityPresenterModule.class})
public interface AppComponent {
void injectMainActivity(MainActivity mainActivity);
}
MainActivityPresenterModule
#Module
public class MainActivityPresenterModule {
#Provides
MainActivityPresenter provideActivityPresenter(NetworkUtils networkUtils) {
return new MainActivityPresenter(networkUtils);
}
#Provides
NetworkUtils provideNetworkUtils(){
return new NetworkUtils();
}
}
NetworkUtils
public class NetworkUtils {
public boolean isConnection() {
return true;
}
}
MainActivityPresenter
public class MainActivityPresenter {
NetworkUtils networkUtils;
public MainActivityPresenter(NetworkUtils networkUtils) {
this.networkUtils = networkUtils;
}
public void getUser(){
if(networkUtils.isConnection()) {
Log.e("getUser", "getUser");
} else {
Log.e("no internet", "no internet connection");
}
}
}
MainActivity
public class MainActivity extends AppCompatActivity {
#Inject
MainActivityPresenterModule mainActivityPresenterModule;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
App.getComponent().injectMainActivity(MainActivity.this);
}
}
You can inject only the things that are provided in the classes annotated with #Module (only methods inside that module which are annotated with #Provides). So, you can do #Inject MainActivityPresenter presenter, for instance, not try to inject the whole module, like you tried to do. Modules should be registered on Dagger initialisation, like this (in App#onCreate)
component = DaggerAppComponent.builder()
.mainActivityPresenterModule(MainActivityPresenterModule())
.build()
In MainActivity you only need to call inject to be able to inject your #Inject MainActivityPresenter presenter or any other injects defined in the module, like so:
#Inject MainActivityPresenter presenter
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
(application as App).component.inject(this)
// after #inject(this) above you can start using your injections:
presenter.getUser()
}
Sorry, I wrote code snippets in Kotlin as it was much less to write that way, hopefully you get the idea how it looks in Java.
I just started using Dagger 2 and I found online thousands guides each one with a different implementation and I'm a bit confused now.
So basically this is what I wrote at the moment:
AppModule.java:
#Module
public class AppModule {
Application mApplication;
public AppModule(Application application) {
mApplication = application;
}
#Provides
#Singleton
Application providesApplication() {
return mApplication;
}
}
DataModule.java:
#Module
public class DataModule {
private static final String BASE_URL = "http://beta.fridgewizard.com:9001/api/";
#Provides
#Singleton
NetworkService provideNetworkService() {
return new NetworkService(BASE_URL);
}
#Provides
#Singleton
SharedPreferences provideSharedPreferences(Application app) {
return PreferenceManager.getDefaultSharedPreferences(app);
}
}
PrefsModel.java:
#Module(includes = DataModule.class)
public class PrefsModel {
#Provides
#Singleton
QueryPreferences provideQuery(SharedPreferences prefs) {
return new QueryPreferences(prefs);
}
}
AppComponent.java (I'm exposing QueryPreferences object since I need it in a presenter, hopefully is correct in this way):
#Singleton
#Component(modules = {AppModule.class, DataModule.class, PrefsModel.class})
public interface AppComponent {
void inject(HomeFragment homeFragment);
QueryPreferences preferences();
NetworkService networkService();
}
Then I have the FwApplication.java:
public class FwApplication extends Application {
private static final String TAG = "FwApplication";
private NetworkService mNetworkService;
private AppComponent mDataComponent;
#Override
public void onCreate() {
super.onCreate();
buildComponentAndInject();
}
public static AppComponent component(Context context) {
return ((FwApplication) context.getApplicationContext()).mDataComponent;
}
public void buildComponentAndInject() {
mDataComponent = DaggerComponentInitializer.init(this);
}
public static final class DaggerComponentInitializer {
public static AppComponent init(FwApplication app) {
return DaggerAppComponent.builder()
.appModule(new AppModule(app))
.dataModule(new DataModule())
.build();
}
}
}
Finally I added another module for the presenters:
#Module
public class PresenterModule {
#Provides
Presenter<FwView> provideHomePresenter(NetworkService networkService) {
return new HomePresenterImpl(networkService);
}
#Provides
Presenter<FwView> provideSearchPresenter(NetworkService networkService) {
return new SearchPresenterImpl(networkService);
}
}
And the following component (which returns error because I cannot add a scoped dependencies here):
#Component(dependencies = AppComponent.class, modules = PresenterModule.class)
public interface PresenterComponent {
void inject(HomePresenterImpl presenter);
}
So, I have few questions that are not clear for me reading the documentation online:
How can I fix the error in the presenter component since it depends on NetworkService which is a singleton defined in the AppComponent?
I have an HomeFragment which should implement the HomePresenter with "new HomePresenter(networkService)" but now I don't know how to use the DI defined
EDIT - FIX:
HomeFragment.java:
public class HomeFragment extends Fragment {
private static final String TAG = "FW.HomeFragment";
#Inject
HomePresenterImpl mHomePresenter;
public static HomeFragment newInstance() {
return new HomeFragment();
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FwApplication.component(getActivity()).inject(this);
}
Then I modified the presenter constructor in this way:
#Inject
public HomePresenterImpl(NetworkService networkService) {
mNetworkService = networkService;
mInteractor = new InteractorImpl(mNetworkService);
}
Then NetworkService is injected automatically.
I was wondering if it is correct in this way since I have to call for every fragment I have that needs a presenter constructed in the same way as the one above the following code:
FwApplication.component(getActivity()).inject(this);
You are mixing thing up. To provide your presenter, you should switch to something like the following:
Use constructor injection if possible. It will make things much easier
public class HomePresenterImpl {
#Inject
public HomePresenterImpl(NetworkService networkService) {
// ...
}
}
To provide the interface use this constructor injection and depend on the implementation:
Presenter<FwView> provideHomePresenter(HomePresenterImpl homePresenter) {
return homePresenter;
}
This way you don't have to call any constructors yourself. And to actually inject the presenter...
public class MyFragment extends Fragment {
#Inject
Presenter<FwView> mHomePresenter;
public void onCreate(Bundle xxx) {
// simplified. Add your modules / Singleton component
PresenterComponent component = DaggerPresenterComponent.create().inject(this);
}
}
This way you will inject the things. Please read this carefully and try to understand it. This will fix your major problems, you still can not provide 2 presenters of the same type from the same module (in the same scope)
// DON'T
#Provides
Presenter<FwView> provideHomePresenter(NetworkService networkService) { /**/ }
#Provides
Presenter<FwView> provideSearchPresenter(NetworkService networkService) { /**/ }
This will not work. You can not provide 2 objects of the same kind. They are indistinguishable. Have a look at #Qualifiers like #Named if you are sure this is the way you want to go.
You do not have to provide Presenter if #Inject annotation is used in the constructor. #Inject annotation used in the constructor of the class makes that class a part of dependencies graph. So, it also can be injected when needed.
On the other hand, if you add #Inject annotation to fields, but not to constructors, you have to provide that class.