android - How can I swap test doubles at the scope of an Activity or a Fragment using Dagger 2? -


edit: watch out! have deleted old repository reffered in question. see own answer question possible solution , feel free improve it!

i refering post here. came little further. refering 2 branches within github project:

  • experimental [branch no. 1] (repository deleted)
  • experimental [branch no. 2] (repository deleted)

in old post tried swap components test-components within instrumentation test. works if have applicationcomponent, being in singleton scope. not work if have activitycomponent self defined @peractivity scope. problem not scope swapping of component testcomponent.

my activitycomponent has activitymodule:

@peractivity @component(modules = activitymodule.class) public interface activitycomponent {     // todo: comment out switching old approach     void inject(mainfragment mainfragment);     // todo: leave witching new approach     void inject(mainactivity mainactivity); } 

activitymodule provides maininteractor

@module public class activitymodule {     @provides     @peractivity     maininteractor providemaininteractor () {         return new maininteractor();     } } 

my testactivitycomponent uses testactivitymodule:

@peractivity @component(modules = testactivitymodule.class) public interface testactivitycomponent extends activitycomponent {     void inject(mainactivitytest mainactivitytest); } 

testactvitymodule provides fakeinteractor :

@module public class testactivitymodule {     @provides     @peractivity     maininteractor providemaininteractor () {         return new fakemaininteractor();     } } 

my mainactivity has getcomponent() method , setcomponent() method. latter can swap component test component within instrumentation test. here activity:

public class mainactivity extends baseactivity implements mainfragment.onfragmentinteractionlistener {       private static final string tag = "mainactivity";     private fragment currentfragment;     private activitycomponent activitycomponent;       @override     protected void oncreate(bundle savedinstancestate) {         super.oncreate(savedinstancestate);         setcontentview(r.layout.activity_main);          initializeinjector();           if (savedinstancestate == null) {             currentfragment = new mainfragment();             addfragment(r.id.fragmentcontainer, currentfragment);         }      }      private void initializeinjector() {         log.i(tag, "injectdagger initializeinjector()");          activitycomponent = daggeractivitycomponent.builder()                 .activitymodule(new activitymodule())                 .build();         activitycomponent.inject(this);     }      @override     public void onfragmentinteraction(final uri uri) {      }      activitycomponent getactivitycomponent() {         return activitycomponent;     }      @visiblefortesting     public void setactivitycomponent(activitycomponent activitycomponent) {         log.w(tag, "injectdagger call method swap test doubles");         this.activitycomponent = activitycomponent;     } }  

as see activity uses mainfragment. in oncreate() of fragment component injected:

public class mainfragment extends basefragment implements mainview {      private static final string tag = "mainfragment";     @inject     mainpresenter mainpresenter;     private view view;      public mainfragment() {         // required empty public constructor     }      @override     public void oncreate(bundle savedinstancestate) {         log.i(tag, "injectdagger oncreate()");         super.oncreate(savedinstancestate);         // todo: approach works //        ((androidapplication)((mainactivity) getactivity()).getapplication()).getapplicationcomponent().inject(this);         // todo: approach not working, see mainactvitytest         ((mainactivity) getactivity()).getactivitycomponent().inject(this);     } } 

and in test swap activitycomponent testapplicationcomponent:

public class mainactivitytest{      @rule     public activitytestrule<mainactivity> mactivityrule = new activitytestrule(mainactivity.class, true, false);      private mainactivity mactivity;     private testactivitycomponent mtestactivitycomponent;      // todo: approach works //    private testapplicationcomponent mtestapplicationcomponent; // //    private void initializeinjector() { //        mtestapplicationcomponent = daggertestapplicationcomponent.builder() //                .testapplicationmodule(new testapplicationmodule(getapp())) //                .build(); // //        getapp().setapplicationcomponent(mtestapplicationcomponent); //        mtestapplicationcomponent.inject(this); //    }      // todo: approach not work because mactivity.setactivitycomponent() called after maininteractor has been injected!     private void initializeinjector() {         mtestactivitycomponent = daggertestactivitycomponent.builder()                 .testactivitymodule(new testactivitymodule())                 .build();          mactivity.setactivitycomponent(mtestactivitycomponent);         mtestactivitycomponent.inject(this);     }      public androidapplication getapp() {         return (androidapplication) instrumentationregistry.getinstrumentation().gettargetcontext().getapplicationcontext();     }     // todo: approach works  //    @before //    public void setup() throws exception { // //        initializeinjector(); //        mactivityrule.launchactivity(null); //        mactivity = mactivityrule.getactivity(); //    }      // todo: approach not works because mactivity.setactivitycomponent() called after maininteractor has been injected!     @before     public void setup() throws exception {         mactivityrule.launchactivity(null);         mactivity = mactivityrule.getactivity();         initializeinjector();     }       @test     public void testonclick_fake() throws exception {         onview(withid(r.id.edittext)).perform(typetext("john"));         onview(withid(r.id.button)).perform(click());         onview(withid(r.id.textview_greeting)).check(matches(withtext(containsstring("hello fake"))));     }      @test     public void testonclick_real() throws exception {         onview(withid(r.id.edittext)).perform(typetext("john"));         onview(withid(r.id.button)).perform(click());         onview(withid(r.id.textview_greeting)).check(matches(withtext(containsstring("hello john"))));     }  } 

the activity test runs wrong component used. because activities , fragments oncreate() run before component swapped.

as can see have commented old approach bind applicationcomponent application class. works because can build dependency before starting activity. activitycomponent have launch activity before initializing injector. because otherwise not set

mactivity.setactivitycomponent(mtestactivitycomponent); 

because mactivity null if launch activity after initialization of injector. (see mainactivitytest)

so how intercept mainactivity , mainfragment use testactivitycomponent?

now found out mixing examples how exchange activity-scoped component , fragment-scoped component. in post show how both. describe in more detail how swap fragment-scoped component during instrumentationtest. total code hosted on github. can run mainfragmenttest class aware have set de.xappo.presenterinjection.runner.androidapplicationjunitrunner testrunner in android studio.

now describe shortly swap interactor fake interactor. in example try respect clean architecture as possible. may small things break architecture bit. feel free improve.

so, let's start. @ first need own junitrunner:

/**  * own junit runner intercepting activitycomponent injection , swapping  * activitycomponent testactivitycomponent  */ public class androidapplicationjunitrunner extends androidjunitrunner {     @override     public application newapplication(classloader classloader, string classname, context context)             throws instantiationexception, illegalaccessexception, classnotfoundexception {         return super.newapplication(classloader, testandroidapplication.class.getname(), context);     }      @override     public activity newactivity(classloader classloader, string classname, intent intent)             throws instantiationexception, illegalaccessexception, classnotfoundexception {         activity activity = super.newactivity(classloader, classname, intent);         return swapactivitygraph(activity);     }      @suppresswarnings("unchecked")     private activity swapactivitygraph(activity activity) {         if (!(activity instanceof hascomponent) || !testactivitycomponentholder.hascomponentcreator()) {             return activity;         }          ((hascomponent<activitycomponent>) activity).                 setcomponent(testactivitycomponentholder.getcomponent(activity));          return activity;     } } 

in swapactivitygraph() create alternative testactivitygraph activity before(!) activity created when running test. have create testfragmentcomponent:

@perfragment @component(modules = testfragmentmodule.class) public interface testfragmentcomponent extends fragmentcomponent{     void inject(mainactivitytest mainactivitytest);      void inject(mainfragmenttest mainfragmenttest); } 

this component lives in fragment-scope. has module:

@module public class testfragmentmodule {     @provides     @perfragment     maininteractor providemaininteractor () {         return new fakemaininteractor();     } } 

the original fragmentmodule looks that:

@module public class fragmentmodule {     @provides     @perfragment     maininteractor providemaininteractor () {         return new maininteractor();     } } 

you see use maininteractor , fakemaininteractor. both that:

public class maininteractor {     private static final string tag = "maininteractor";      public maininteractor() {         log.i(tag, "constructor");     }      public person createperson(final string name) {         return new person(name);     } }   public class fakemaininteractor extends maininteractor {     private static final string tag = "fakemaininteractor";      public fakemaininteractor() {         log.i(tag, "constructor");     }      public person createperson(final string name) {         return new person("fake person");     } } 

now use self-defined fragmenttestrule testing fragment independent activity contains in production:

public class fragmenttestrule<f extends fragment> extends activitytestrule<testactivity> {     private static final string tag = "fragmenttestrule";     private final class<f> mfragmentclass;     private f mfragment;      public fragmenttestrule(final class<f> fragmentclass) {         super(testactivity.class, true, false);         mfragmentclass = fragmentclass;     }      @override     protected void beforeactivitylaunched() {         super.beforeactivitylaunched();         try {             mfragment = mfragmentclass.newinstance();         } catch (instantiationexception e) {             e.printstacktrace();         } catch (illegalaccessexception e) {             e.printstacktrace();         }     }      @override     protected void afteractivitylaunched() {         super.afteractivitylaunched();          //instantiate , insert fragment container layout         fragmentmanager manager = getactivity().getsupportfragmentmanager();         fragmenttransaction transaction = manager.begintransaction();          transaction.replace(r.id.fragmentcontainer, mfragment);         transaction.commit();     }       public f getfragment() {         return mfragment;     } } 

that testactivity simple:

public class testactivity extends baseactivity implements         hascomponent<activitycomponent> {      @override     protected void oncreate(@nullable final bundle savedinstancestate) {         super.oncreate(savedinstancestate);         framelayout framelayout = new framelayout(this);         framelayout.setid(r.id.fragmentcontainer);         setcontentview(framelayout);     } } 

but how swap components? there several small tricks achieve that. @ first need holder class holding testfragmentcomponent:

/**  * because neither activity nor activitytest can hold testactivitycomponent (due  * runtime order problems need hold statically  **/ public class testfragmentcomponentholder {     private static testfragmentcomponent scomponent;     private static componentcreator screator;      public interface componentcreator {         testfragmentcomponent createcomponent(fragment fragment);     }      /**      * configures componentcreator used create activity graph. call in @before.      *      * @param creator creator      */     public static void setcreator(componentcreator creator) {         screator = creator;     }      /**      * releases static instances of our creator , graph. call in @after.      */     public static void release() {         screator = null;         scomponent = null;     }      /**      * returns {@link testfragmentcomponent} or creates new 1 using registered {@link      * componentcreator}      *      * @throws illegalstateexception if no creator has been registered before      */     @nonnull     public static testfragmentcomponent getcomponent(fragment fragment) {         if (scomponent == null) {             checkregistered(screator != null, "no creator registered");             scomponent = screator.createcomponent(fragment);         }         return scomponent;     }      /**      * returns true if custom activity component creator configured current test run,      * false otherwise      */     public static boolean hascomponentcreator() {         return screator != null;     }      /**      * returns instantiated {@link testfragmentcomponent}.      *      * @throws illegalstateexception if none has been instantiated      */     @nonnull     public static testfragmentcomponent getcomponent() {         checkregistered(scomponent != null, "no component created");         return scomponent;     } } 

the second trick use holder register component before fragment created. launch testactivity our fragmenttestrule. comes third trick timing-dependent , not run correctly. directly after launching activity fragment instance asking fragmenttestrule. swap component, using testfragmentcomponentholder , inject fragment graph. forth trick wait 2 seconds fragment created. , within fragment make our component injection in onviewcreated(). because don't inject component because oncreate() , oncreateview() called before. here our mainfragment:

public class mainfragment extends basefragment implements mainview {      private static final string tag = "mainfragment";     @inject     mainpresenter mainpresenter;     private view view;      // todo: rename , change types , number of parameters     public static mainfragment newinstance() {         mainfragment fragment = new mainfragment();         return fragment;     }      public mainfragment() {         // required empty public constructor     }      @override     public void oncreate(bundle savedinstancestate) {         super.oncreate(savedinstancestate);          //((mainactivity)getactivity()).getcomponent().inject(this);     }      @override     public view oncreateview(layoutinflater inflater, viewgroup container,             bundle savedinstancestate) {         view = inflater.inflate(r.layout.fragment_main, container, false);         return view;     }      public void onclick(final string s) {         mainpresenter.onclick(s);     }      @override     public void onviewcreated(final view view, @nullable final bundle savedinstancestate) {         super.onviewcreated(view, savedinstancestate);         getcomponent().inject(this);          final edittext edittext = (edittext) view.findviewbyid(r.id.edittext);         button button = (button) view.findviewbyid(r.id.button);         button.setonclicklistener(new view.onclicklistener() {             @override             public void onclick(final view v) {                 mainfragment.this.onclick(edittext.gettext().tostring());             }         });         mainpresenter.attachview(this);     }      @override     public void updateperson(final person person) {         textview textview = (textview) view.findviewbyid(r.id.textview_greeting);         textview.settext("hello " + person.getname());     }      @override     public void ondestroy() {         super.ondestroy();         mainpresenter.detachview();     }      public interface onfragmentinteractionlistener {         void onfragmentinteraction(uri uri);     } } 

and steps (second forth trick) described before can found in @before annotated setup()-method in mainfragmenttest class:

public class mainfragmenttest implements         injectscomponent<testfragmentcomponent>, testfragmentcomponentholder.componentcreator {      private static final string tag = "mainfragmenttest";     @rule     public fragmenttestrule<mainfragment> mfragmenttestrule = new fragmenttestrule<>(mainfragment.class);      public androidapplication getapp() {         return (androidapplication) instrumentationregistry.getinstrumentation().gettargetcontext().getapplicationcontext();     }      @before     public void setup() throws exception {         testfragmentcomponentholder.setcreator(this);          mfragmenttestrule.launchactivity(null);          mainfragment fragment = mfragmenttestrule.getfragment();          if (!(fragment instanceof hascomponent) || !testfragmentcomponentholder.hascomponentcreator()) {             return;         } else {             ((hascomponent<fragmentcomponent>) fragment).                     setcomponent(testfragmentcomponentholder.getcomponent(fragment));              injectfragmentgraph();              waitforfragment(r.id.fragmentcontainer, 2000);         }     }      @after     public void teardown() throws  exception {         testfragmentcomponentholder.release();         mfragmenttestrule = null;     }      @suppresswarnings("unchecked")     private void injectfragmentgraph() {         ((injectscomponent<testfragmentcomponent>) this).injectcomponent(testfragmentcomponentholder.getcomponent());     }      protected fragment waitforfragment(@idres int id, int timeout) {         long endtime = systemclock.uptimemillis() + timeout;         while (systemclock.uptimemillis() <= endtime) {              fragment fragment = mfragmenttestrule.getactivity().getsupportfragmentmanager().findfragmentbyid(id);             if (fragment != null) {                 return fragment;             }         }         return null;     }      @override     public testfragmentcomponent createcomponent(final fragment fragment) {         return daggertestfragmentcomponent.builder()                 .testfragmentmodule(new testfragmentmodule())                 .build();     }      @test     public void testonclick_fake() throws exception {         onview(withid(r.id.edittext)).perform(typetext("john"));         onview(withid(r.id.button)).perform(click());         onview(withid(r.id.textview_greeting)).check(matches(withtext(containsstring("hello fake"))));     }      @test     public void testonclick_real() throws exception {         onview(withid(r.id.edittext)).perform(typetext("john"));         onview(withid(r.id.button)).perform(click());         onview(withid(r.id.textview_greeting)).check(matches(withtext(containsstring("hello john"))));     }       @override     public void injectcomponent(final testfragmentcomponent component) {         component.inject(this);     } } 

except timing problem. test runs in environment in 10 of 10 test runs on emulated android api level 23. , runs in 9 of 10 test runs on real samsung galaxy s5 neo device android 6.

as wrote above can download whole example github , feel free improve if find way fix little timing problem.

that's it!


Comments

Popular posts from this blog

php - How to add and update images or image url in Volusion using Volusion API -

javascript - jQuery UI Splitter/Resizable for unlimited amount of columns -

javascript - IE9 error '$'is not defined -