2014-11-14 2 views
50

У меня есть simple Android activity с одной зависимостью. Я впрыскивать зависимость в деятельность onCreate-х, как это:Как вы переопределяете модуль/зависимость в модульном тесте с помощью Dagger 2.0?

Dagger_HelloComponent.builder() 
    .helloModule(new HelloModule(this)) 
    .build() 
    .initialize(this); 

В моей ActivityUnitTestCase я хочу, чтобы переопределить зависимость насмешливо Mockito. Я предполагаю, что мне нужно использовать тестовый модуль, который предоставляет макет, но я не могу понять, как добавить этот модуль к графику объекта.

В Dagger 1.x это, по-видимому сделано с something like this:

@Before 
public void setUp() { 
    ObjectGraph.create(new TestModule()).inject(this); 
} 

Что Dagger 2,0 эквивалент выше?

Вы можете увидеть мой проект и его модульный тест here on GitHub.

+4

Вот видео Джейк Wharton от Devoxx 2014: https://plus.google.com/+JakeWharton/posts/SRaaHenwLfj , в котором он упоминает (в 0:45:40), что модуль Overrides еще не поддерживается во время презентации. –

+2

Следуйте приведенным здесь обсуждениям: https://github.com/google/dagger/issues/110 –

ответ

39

Возможно, это скорее обходной путь, обеспечивающий правильную поддержку модуля тестирования, но он позволяет переопределять производственные модули с тестовым. В приведенных ниже фрагментах кода показан простой случай, когда у вас есть только один компонент и один модуль, но это должно работать для любого сценария. Для этого требуется много повторений шаблонов и кодов, поэтому имейте это в виду. Я уверен, что в будущем будет лучший способ добиться этого.

Я также создал project with examples for Espresso and Robolectric. Этот ответ основан на коде, содержащемся в проекте.

решение требует двух вещей:

  • обеспечивают дополнительную сеттера для @Component
  • тест компонент должен расширить компонент производства

Предположим, мы в простой Application, как показано ниже:

public class App extends Application { 

    private AppComponent mAppComponent; 

    @Override 
    public void onCreate() { 
     super.onCreate(); 
     mAppComponent = DaggerApp_AppComponent.create(); 
    } 

    public AppComponent component() { 
     return mAppComponent; 
    } 

    @Singleton 
    @Component(modules = StringHolderModule.class) 
    public interface AppComponent { 

     void inject(MainActivity activity); 
    } 

    @Module 
    public static class StringHolderModule { 

     @Provides 
     StringHolder provideString() { 
      return new StringHolder("Release string"); 
     } 
    } 
} 

Мы должны добавить additio nal до App класс. Это позволяет нам заменить производственный компонент.

/** 
* Visible only for testing purposes. 
*/ 
// @VisibleForTesting 
public void setTestComponent(AppComponent appComponent) { 
    mAppComponent = appComponent; 
} 

Как вы можете видеть StringHolder объект содержит "строку Release" значение. Этот объект вводится в MainActivity.

public class MainActivity extends ActionBarActivity { 

    @Inject 
    StringHolder mStringHolder; 

    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
     super.onCreate(savedInstanceState); 
     setContentView(R.layout.activity_main); 
     ((App) getApplication()).component().inject(this); 
    } 
} 

В наших тестах мы хотим обеспечить StringHolder с «Тест строки». Мы должны установить тестовый компонент в классе App до создания MainActivity - потому что StringHolder вводится в обратном вызове onCreate.

В компонентах Dagger v2.0.0 могут распространяться и другие интерфейсы.Мы можем использовать это, чтобы создать наш TestAppComponent, который распространяется наAppComponent.

@Component(modules = TestStringHolderModule.class) 
interface TestAppComponent extends AppComponent { 

} 

Теперь мы можем определить наши тестовые модули, например. TestStringHolderModule. Последний шаг - установить тестовый компонент с использованием ранее добавленного метода установки в классе App. Это важно сделать до создания активности.

((App) application).setTestComponent(mTestAppComponent); 

Эспрессо

Для Эспрессо Я создал пользовательский ActivityTestRule, который позволяет менять компонент перед деятельности создается. Вы можете найти код для DaggerActivityTestRulehere.

Примеры тестов с Espresso:

@RunWith(AndroidJUnit4.class) 
@LargeTest 
public class MainActivityEspressoTest { 

    public static final String TEST_STRING = "Test string"; 

    private TestAppComponent mTestAppComponent; 

    @Rule 
    public ActivityTestRule<MainActivity> mActivityRule = 
      new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener<MainActivity>() { 
       @Override 
       public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) { 
        mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create(); 
        ((App) application).setTestComponent(mTestAppComponent); 
       } 
      }); 

    @Component(modules = TestStringHolderModule.class) 
    interface TestAppComponent extends AppComponent { 

    } 

    @Module 
    static class TestStringHolderModule { 

     @Provides 
     StringHolder provideString() { 
      return new StringHolder(TEST_STRING); 
     } 
    } 

    @Test 
    public void checkSomething() { 
     // given 
     ... 

     // when 
     onView(...) 

     // then 
     onView(...) 
       .check(...); 
    } 
} 

Robolectric

Это гораздо проще с Robolectric благодаря RuntimeEnvironment.application.

Примеры тестов с Robolectric:

@RunWith(RobolectricGradleTestRunner.class) 
@Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class) 
public class MainActivityRobolectricTest { 

    public static final String TEST_STRING = "Test string"; 

    @Before 
    public void setTestComponent() { 
     AppComponent appComponent = DaggerMainActivityRobolectricTest_TestAppComponent.create(); 
     ((App) RuntimeEnvironment.application).setTestComponent(appComponent); 
    } 

    @Component(modules = TestStringHolderModule.class) 
    interface TestAppComponent extends AppComponent { 

    } 

    @Module 
    static class TestStringHolderModule { 

     @Provides 
     StringHolder provideString() { 
      return new StringHolder(TEST_STRING); 
     } 
    } 

    @Test 
    public void checkSomething() { 
     // given 
     MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class); 

     // when 
     ... 

     // then 
     assertThat(...) 
    } 
} 
+1

Единственная проблема с этим в том, что я не могу расширять StringHolderModule. Поэтому, если в моем модуле много провайдеров, мне нужно скопировать/вставить и высмеять только тот, который я хочу. Помимо этого, эти ответы являются cooooorrect!/o/ – Tsuharesu

+2

@Tsuharesu "и высмеивать только тот, который я хочу" - может быть, вы должны разделить ваши модули? Это «плохой запах», чтобы все методы предоставлялись только в одном модуле. Это предотвращает повторное использование кода. Создайте модуль для разных видов зависимостей, и вы легко избавитесь от этой проблемы. – tomrozb

+0

у моего AppModule, например, есть вещи, которые зависят от контекста приложения, например SharedPreferences и фактического экземпляра приложения (поэтому я могу инициализировать Prefs). Так что даже этот крошечный модуль имеет 2, и мне нужно будет насмехаться. В любом случае, я начинаю больше узнавать о кинжале и DI вообще, спасибо за подсказку :) – Tsuharesu

-4

С помощью Dagger2 вы можете передать определенный модуль (TestModule) в компонент, используя сгенерированный строитель api.

ApplicationComponent appComponent = Dagger_ApplicationComponent.builder() 
       .helloModule(new TestModule()) 
       .build(); 

Пожалуйста, обратите внимание, что, Dagger_ApplicationComponent это сгенерированный класс с новым @Component аннотации.

+3

Это не сработает. Метод 'helloModule' принимает только экземпляр' HelloModule' и поскольку модули не могут распространяться на другие классы, TestModule не может работать. –

+0

Спасибо Джейк Уортон, хотя TestModule может расширять HelloModule. Является ли это целью или есть какая-либо открытая проблема в dagger2 github, связанная с этим? –

+1

Нет проблем. Это хорошо известная вещь, которую мы обсуждали. –

1

Этот ответ ИСП. ПРОЧИТАЙТЕ НИЖЕ ИЗМЕНЕНИЯ.

достаточно Disappointingly, вы не может простираться от модуля, или вы получите следующее сообщение об ошибке компиляции:

Error:(24, 21) error: @Provides methods may not override another method. 
Overrides: Provides 
    retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint() 

означает, что вы не можете просто расширить «макет модуля» и заменить оригинал модуль. Нет, это не так просто. И учитывая, что вы разрабатываете свои компоненты таким образом, чтобы они напрямую связывали модули по классам, вы не можете просто просто сделать «TestComponent», потому что это означает, что вам нужно повторно изобрести все с нуля, d должен составлять компонент для каждой вариации! Ясно, что это не вариант.

Итак, в меньшем масштабе то, что я закончил, делает «поставщик», который я предоставляю модулю, который определяет, выбираю ли я макет или тип производства.

public interface EndpointProvider { 
    Endpoint serverEndpoint(); 
} 

public class ProdEndpointProvider implements EndpointProvider { 

    @Override 
    public Endpoint serverEndpoint() { 
     return new ServerEndpoint(); 
    } 
} 


public class TestEndpointProvider implements EndpointProvider { 
    @Override 
    public Endpoint serverEndpoint() { 
     return new TestServerEndpoint(); 
    } 
} 

@Module 
public class EndpointModule { 
    private Endpoint serverEndpoint; 

    private EndpointProvider endpointProvider; 

    public EndpointModule(EndpointProvider endpointProvider) { 
     this.endpointProvider = endpointProvider; 
    } 

    @Named("server") 
    @Provides 
    public Endpoint serverEndpoint() { 
     return endpointProvider.serverEndpoint(); 
    } 
} 

EDIT: Видимо, как говорится в сообщении об ошибке, вы не можете переопределить другой метод с использованием @Provides аннотированный метод, но это не означает, что вы не можете переопределить @Provides аннотированный метод :(

Все это волшебство было напрасно! Вы можете просто расширить модуль, не помещая на метод, и он работает ... Обратитесь к ответу @vaughandroid.

8

Обходной путь, предложенный @tomrozb, очень хорош и надел меня правильный трек, но моя проблема заключалась в том, что он обнаружил setTestComponent() метод в ПРОИЗВОДСТВЕ Application класс. Мне удалось заставить это работать немного по-другому, так что мое производственное приложение не должно вообще ничего знать о моей тестовой среде.

TL; DR - Расширьте класс приложения с помощью тестового приложения, которое использует ваш тестовый компонент и модуль. Затем создайте собственный тестовый бегун, который запускается в тестовом приложении вместо вашего производственного приложения.


EDIT: Этот метод работает только для глобальных зависимостей (как правило, с пометкой @Singleton). Если ваше приложение имеет компоненты с разной областью действия (например, для каждого действия), вам нужно будет либо создавать подклассы для каждой области, либо использовать исходный ответ @ tomrozb. Спасибо @tomrozb за это!


В этом примере используется AndroidJUnitRunner тестов-бегуна, но это, вероятно, может быть адаптировано к Robolectric и другим.

Во-первых, мое приложение для производства. Это выглядит примерно так:

public class MyApp extends Application { 
    protected MyComponent component; 

    public void setComponent() { 
     component = DaggerMyComponent.builder() 
       .myModule(new MyModule()) 
       .build(); 
     component.inject(this); 
    } 

    public MyComponent getComponent() { 
     return component; 
    } 

    @Override 
    public void onCreate() { 
     super.onCreate(); 
     setComponent(); 
    } 
} 

Таким образом, мои действия и другого класса, которые используют @Inject просто назвать что-то вроде getApp().getComponent().inject(this); вводить себя в графе зависимостей.

Для полноты, вот мой компонент:

@Singleton 
@Component(modules = {MyModule.class}) 
public interface MyComponent { 
    void inject(MyApp app); 
    // other injects and getters 
} 

И мой модуль:

@Module 
public class MyModule { 
    // EDIT: This solution only works for global dependencies 
    @Provides @Singleton 
    public MyClass provideMyClass() { ... } 

    // ... other providers 
} 

Для среды тестирования, продлить тестовый компонент вашего производственного компонента. Это то же самое, что и в ответе @ tomrozb.

@Singleton 
@Component(modules = {MyTestModule.class}) 
public interface MyTestComponent extends MyComponent { 
    // more component methods if necessary 
} 

И тестовый модуль может быть любым, что вы хотите. Предположительно, вы будете обрабатывать свое издевательство и прочее здесь (я использую Mockito).

@Module 
public class MyTestModule { 
    // EDIT: This solution only works for global dependencies 
    @Provides @Singleton 
    public MyClass provideMyClass() { ... } 

    // Make sure to implement all the same methods here that are in MyModule, 
    // even though it's not an override. 
} 

Так что теперь сложная часть. Создайте класс тестового приложения, который простирается от вашего производственного класса приложения, и переопределите метод setComponent(), чтобы установить тестовый компонент с помощью тестового модуля. Обратите внимание, что это может работать, только если MyTestComponent является потомком MyComponent.

public class MyTestApp extends MyApp { 

    // Make sure to call this method during setup of your tests! 
    @Override 
    public void setComponent() { 
     component = DaggerMyTestComponent.builder() 
       .myTestModule(new MyTestModule()) 
       .build(); 
     component.inject(this) 
    } 
} 

Убедитесь, что вы звоните setComponent() на приложение, прежде чем начать свои тесты, чтобы убедиться, что граф настроен правильно. Что-то вроде этого:

@Before 
public void setUp() { 
    MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext(); 
    app.setComponent() 
    ((MyTestComponent) app.getComponent()).inject(this) 
} 

Наконец, последний недостающий кусок, чтобы переопределить TestRunner с пользовательским тестовым бегуном. В моем проекте я использовал AndroidJUnitRunner, но похоже, что вы можете do the same with Robolectric.

public class TestRunner extends AndroidJUnitRunner { 
    @Override 
    public Application newApplication(@NonNull ClassLoader cl, String className, Context context) 
      throws InstantiationException, IllegalAccessException, ClassNotFoundException { 
     return super.newApplication(cl, MyTestApp.class.getName(), context); 
    } 
} 

Вы также должны обновить testInstrumentationRunner Gradle, например, так:

testInstrumentationRunner "com.mypackage.TestRunner" 

И если вы используете Android Studio, вы также должны нажать Edit Configuration из меню запуска и введите имя вашего тестового бегуна в разделе «Специфический контролер».

И все! Надеюсь, эта информация помогает кому-то :)

+0

Что относительно компонентов с разной степенью (например, за активность)? Это работает только для компонентов, созданных в объекте Application, таким образом решает проблемы для простых приложений. В больших приложениях, использующих другие области, вам все равно придется использовать публичный сеттер. Я не говорю, что это решение неверно, но оно не распространяется на все варианты использования. – tomrozb

+2

Это ИМО самый чистый путь на данный момент (хотя я предпочитаю переопределять 'getComponent', а не иметь' setComponent', даже частный, но результат тот же). * Для тестов с robolectric-run * можно указать класс приложения, аннотируя тестовый класс или метод с помощью '@Config (application = MyTestApp.class)'. – desseim

+0

@tomrozb Как так? Типичная введенная активность, согласно вашему примеру, будет извлекать компонент приложения из контекста приложения сама по себе, тем самым получая компонент тестового приложения при запуске в тесте (в соответствии с решением этого ответа). – desseim

20

Как @EpicPandaForce справедливо говорит, вы не можете продлить модули. Тем не менее, я придумал проворное обходное решение для этого, которое, я думаю, избегает большого количества шаблонов, от которых страдают другие примеры.

Уловкой для «расширения» модуля является создание частичного макета и макетирование методов провайдера, которые вы хотите переопределить.

Использование Mockito:

MyModule module = Mockito.spy(new MyModule()); 
Mockito.doReturn("mocked string").when(module).provideString(); 

MyComponent component = DaggerMyComponent.builder() 
     .myModule(module) 
     .build(); 

app.setComponent(component); 

Я создал this gist здесь, чтобы показать полный пример.

EDIT

Оказывается, вы можете сделать это даже без частичного издеваться, например, так:

MyComponent component = DaggerMyComponent.builder() 
     .myModule(new MyModule() { 
      @Override public String provideString() { 
       return "mocked string"; 
      } 
     }) 
     .build(); 

app.setComponent(component); 
0

Можете ли вы, ребята, проверить мое решение, я включил подкомпонент пример: https://github.com/nongdenchet/android-mvvm-with-tests. Спасибо @vaughandroid, я позаимствовал ваши основные методы. Вот главный пункт:

  1. Я создаю класс для создания подкомпонента. Мой заказ приложение будет также держать экземпляр этого класса:

    // The builder class 
    public class ComponentBuilder { 
    private AppComponent appComponent; 
    
    public ComponentBuilder(AppComponent appComponent) { 
        this.appComponent = appComponent; 
    } 
    
    public PlacesComponent placesComponent() { 
        return appComponent.plus(new PlacesModule()); 
    } 
    
    public PurchaseComponent purchaseComponent() { 
        return appComponent.plus(new PurchaseModule()); 
    } 
    } 
    
    // My custom application class 
    public class MyApplication extends Application { 
    
    protected AppComponent mAppComponent; 
    protected ComponentBuilder mComponentBuilder; 
    
    @Override 
    public void onCreate() { 
        super.onCreate(); 
    
        // Create app component 
        mAppComponent = DaggerAppComponent.builder() 
          .appModule(new AppModule()) 
          .build(); 
    
        // Create component builder 
        mComponentBuilder = new ComponentBuilder(mAppComponent); 
    } 
    
    public AppComponent component() { 
        return mAppComponent; 
    } 
    
    public ComponentBuilder builder() { 
        return mComponentBuilder; 
    } 
    } 
    
    // Sample using builder class: 
    public class PurchaseActivity extends BaseActivity { 
    ...  
    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        ... 
        // Setup dependency 
        ((MyApplication) getApplication()) 
          .builder() 
          .purchaseComponent() 
          .inject(this); 
        ... 
    } 
    } 
    
  2. У меня есть пользовательский TestApplication, который расширяет класс MyApplication выше. Этот класс содержит два метода, чтобы заменить компонент корня и строитель:

    public class TestApplication extends MyApplication { 
    public void setComponent(AppComponent appComponent) { 
        this.mAppComponent = appComponent; 
    } 
    
    public void setComponentBuilder(ComponentBuilder componentBuilder) { 
        this.mComponentBuilder = componentBuilder; 
    } 
    }  
    
  3. Наконец я попытаюсь издеваться или окурок зависимость модуля и строителем, чтобы обеспечить фальшивое зависимость от активности:

    @MediumTest 
    @RunWith(AndroidJUnit4.class) 
    public class PurchaseActivityTest { 
    
    @Rule 
    public ActivityTestRule<PurchaseActivity> activityTestRule = 
        new ActivityTestRule<>(PurchaseActivity.class, true, false); 
    
    @Before 
    public void setUp() throws Exception { 
    PurchaseModule stubModule = new PurchaseModule() { 
        @Provides 
        @ViewScope 
        public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) { 
         return new StubPurchaseViewModel(); 
        } 
    }; 
    
    // Setup test component 
    AppComponent component = ApplicationUtils.application().component(); 
    ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) { 
        @Override 
        public PurchaseComponent purchaseComponent() { 
         return component.plus(stubModule); 
        } 
    }); 
    
    // Run the activity 
    activityTestRule.launchActivity(new Intent()); 
    } 
    
+0

Спасибо @ SruitA.Suk, я обновлю контент –

2

Кажется, я нашел еще один способ, и он работает до сих пор.

Во-первых, компонент интерфейса, который сам по себе не компонент:

MyComponent.java

interface MyComponent { 
    Foo provideFoo(); 
} 

Тогда мы имеем два различных модулей: один фактический и тестирование один.

MyModule.java

@Module 
class MyModule { 
    @Provides 
    public Foo getFoo() { 
     return new Foo(); 
    } 
} 

TestModule.java

@Module 
class TestModule { 
    private Foo foo; 
    public void setFoo(Foo foo) { 
     this.foo = foo; 
    } 

    @Provides 
    public Foo getFoo() { 
     return foo; 
    } 
} 

И у нас есть два компонента, чтобы использовать эти два модуля:

MyRealComponent.Java

@Component(modules=MyModule.class) 
interface MyRealComponent extends MyComponent { 
    Foo provideFoo(); // without this dagger will not do its magic 
} 

MyTestComponent.java

@Component(modules=TestModule.class) 
interface MyTestComponent extends MyComponent { 
    Foo provideFoo(); 
} 

В приложении мы делаем это:

MyComponent component = DaggerMyRealComponent.create(); 
<...> 
Foo foo = component.getFoo(); 

Пока в тестовом коде мы используем:

TestModule testModule = new TestModule(); 
testModule.setFoo(someMockFoo); 
MyComponent component = DaggerMyTestComponent.builder() 
    .testModule(testModule).build(); 
<...> 
Foo foo = component.getFoo(); // will return someMockFoo 

Проблема заключается в том, что мы должны скопировать все методы MyModule в TestModule, но это может быть сделано с помощью MyModule внутри TestModule и использования методов MyModule, если они не установлены напрямую извне. Как это:

TestModule.java

@Module 
class TestModule { 
    MyModule myModule = new MyModule(); 
    private Foo foo = myModule.getFoo(); 
    public void setFoo(Foo foo) { 
     this.foo = foo; 
    } 

    @Provides 
    public Foo getFoo() { 
     return foo; 
    } 
} 
Смежные вопросы