Юнит-тесты Android с Dagger 2
Проблема с мокированием java.util.Random в приложении на Android с использованием Dagger 2
У меня есть приложение для Android, которое использует Dagger 2 для внедрения зависимостей. Я также использую последние инструменты сборки Gradle, которые позволяют создавать варианты сборки для юнит-тестирования и инструментальных тестов. В приложении я использую класс java.util.Random
, и мне нужно замокировать его для выполнения тестов. Классы, которые я тестирую, не используют никакие Android компоненты, поэтому это обычные Java классы.
В основном коде я определяю Component
в классе, который расширяет класс Application
, но в юнит-тестах я не использую Application
. Я пробовал создать тестовый Module
и Component
, но Dagger не генерирует Component
. Я также попытался использовать Component
, который я определил в приложении, и заменить Module
при его создании, но Component
приложения не имеет методов inject
для моих тестовых классов. Как я могу предоставить мокированное представление Random
для тестирования?
Вот пример кода:
Приложение:
public class PipeGameApplication extends Application {
private PipeGame pipeGame;
@Singleton
@Component(modules = PipeGameModule.class)
public interface PipeGame {
void inject(BoardFragment boardFragment);
void inject(ConveyorFragment conveyorFragment);
}
@Override
public void onCreate() {
super.onCreate();
pipeGame = DaggerPipeGameApplication_PipeGame.create();
}
public PipeGame component() {
return pipeGame;
}
}
Модуль:
@Module
public class PipeGameModule {
@Provides
@Singleton
Random provideRandom() {
return new Random();
}
}
Базовый класс для тестов:
public class BaseModelTest {
PipeGameTest pipeGameTest;
@Singleton
@Component(modules = PipeGameTestModule.class)
public interface PipeGameTest {
void inject(BoardModelTest boardModelTest);
void inject(ConveyorModelTest conveyorModelTest);
}
@Before
public void setUp() {
pipeGameTest = DaggerBaseModelTest_PipeGameTest.create(); // Не работает
}
public PipeGameTest component() {
return pipeGameTest;
}
}
или:
public class BaseModelTest {
PipeGameApplication.PipeGame pipeGameTest;
// Это работает, если я делаю тестовый модуль производным от
// производственного модуля, но он не может внедрить мои тестовые классы.
@Before
public void setUp() {
pipeGameTest = DaggerPipeGameApplication_PipeGame.builder().pipeGameModule(new PipeGameModuleTest()).build();
}
public PipeGameApplication.PipeGame component() {
return pipeGameTest;
}
}
Тестовый модуль:
@Module
public class PipeGameTestModule {
@Provides
@Singleton
Random provideRandom() {
return mock(Random.class);
}
}
Как я могу правильно замокировать Random
для своих тестов, учитывая текущую структуру кода?
4 ответ(ов)
Вы действительно правы, когда говорите, что
компонент вашего приложения не имеет методов для внедрения в мои классы тестов.
Чтобы обойти эту проблему, мы можем создать тестовую версию вашего класса Application, а затем сделать тестовую версию вашего модуля. Для того чтобы всё это работало в тестах, мы можем использовать Robolectric.
- Создайте тестовую версию вашего Application класса:
public class TestPipeGameApp extends PipeGameApp {
private PipeGameModule pipeGameModule;
@Override protected PipeGameModule getPipeGameModule() {
if (pipeGameModule == null) {
return super.pipeGameModule();
}
return pipeGameModule;
}
public void setPipeGameModule(PipeGameModule pipeGameModule) {
this.pipeGameModule = pipeGameModule;
initComponent();
}
}
- **Ваш оригинальный класс Application должен содержать методы initComponent() и pipeGameModule():
public class PipeGameApp extends Application {
protected void initComponent() {
DaggerPipeGameComponent.builder()
.pipeGameModule(getPipeGameModule())
.build();
}
protected PipeGameModule pipeGameModule() {
return new PipeGameModule(this);
}
}
- Ваш PipeGameTestModule должен расширять производственный модуль с конструктором:
public class PipeGameTestModule extends PipeGameModule {
public PipeGameTestModule(Application app) {
super(app);
}
}
- Теперь в методе setup() вашего junit теста установите этот тестовый модуль в вашем тестовом приложении:
@Before
public void setup() {
TestPipeGameApp app = (TestPipeGameApp) RuntimeEnvironment.application;
PipeGameTestModule module = new PipeGameTestModule(app);
app.setPipeGameModule(module);
}
Теперь вы можете настроить ваш тестовый модуль так, как вы изначально хотели.
В вашем случае, я считаю, что можно подойти к решению этой задачи с другой стороны. Вы сможете легко протестировать свой класс, не полагаясь на Dagger для его создания, при этом подставив имитированные зависимости.
Что я имею в виду, так это то, что в настройках теста вы можете:
- Имитировать (mock) зависимости класса, который тестируете.
- Создать класс, который тестируете, вручную, используя имитированные зависимости.
Нам не нужно тестировать, корректно ли внедряются зависимости, так как Dagger проверяет правильность графа зависимостей во время компиляции. Поэтому любые такие ошибки будут выявлены в процессе компиляции. Именно поэтому ручное создание класса, который тестируется, в методе настройки должно быть приемлемым.
Вот пример кода, где зависимость внедряется через конструктор в тестируемом классе:
public class BoardModelTest {
private BoardModel boardModel;
private Random random;
@Before
public void setUp() {
random = mock(Random.class);
boardModel = new BoardModel(random);
}
@Test
...
}
public class BoardModel {
private Random random;
@Inject
public BoardModel(Random random) {
this.random = random;
}
...
}
А вот пример, где зависимость внедряется через поле в тестируемом классе (в случае, если BoardModel
создается фреймворком):
public class BoardModelTest {
private BoardModel boardModel;
private Random random;
@Before
public void setUp() {
random = mock(Random.class);
boardModel = new BoardModel();
boardModel.random = random;
}
@Test
...
}
public class BoardModel {
@Inject
Random random;
public BoardModel() {}
...
}
Таким образом, вы можете подходить к тестированию модульно, не беспокоясь о внедрении зависимостей с помощью Dagger, что значительно упростит процесс тестирования.
Если вы используете Dagger 2 с Android, вы можете воспользоваться flavor'ами приложения для предоставления ресурсов для мокирования.
Посмотрите здесь демонстрацию использования flavor'ов в тестировании моков (без Dagger): https://www.youtube.com/watch?v=vdasFFfXKOY
В этом репозитории есть пример: https://github.com/googlecodelabs/android-testing
В вашем файле /src/prod/com/yourcompany/Component.java вы предоставляете ваши производственные компоненты.
В вашем файле /src/mock/com/yourcompany/Component.java вы предоставляете компоненты для мокирования.
Это позволяет вам создавать сборки вашего приложения с или без моков. Это также позволяет вести параллельную разработку (бэкенд одной командой, фронтенд приложения другой командой), вы можете мокировать до тех пор, пока API методы не станут доступны.
Вот как выглядят мои команды Gradle (это Makefile):
install_mock:
./gradlew installMockDebug
install:
./gradlew installProdDebug
test_unit:
./gradlew testMockDebugUnitTest
test_integration_mock:
./gradlew connectedMockDebugAndroidTest
test_integration_prod:
./gradlew connectedProdDebugAndroidTest
Таким образом, вы можете удобно управлять разными версиями вашего приложения, что значительно упрощает процесс тестирования и разработки.
У меня была та же проблема, и я нашел очень простое решение. Хотя это, возможно, не самое лучшее решение, оно определенно поможет вам.
Создайте аналогичный класс в вашем модуле приложения:
public class ActivityTest<T extends ViewModelBase> {
@Inject
public T vm;
}
Затем в вашем AppComponent добавьте следующий метод:
void inject(ActivityTest<LoginFragmentVM> activityTest);
Теперь вы сможете выполнить инъекцию в вашем тестовом классе.
public class HelloWorldEspressoTest extends ActivityTest<LoginFragmentVM> {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void listGoesOverTheFold() throws InterruptedException {
App.getComponent().inject(this);
vm.email.set("1234");
closeSoftKeyboard();
}
}
Это позволит вам работать с вашей ViewModel в тестах, как вам нужно.
Как исправить 'android.os.NetworkOnMainThreadException'?
Ошибка «Необходимо переопределить метод суперкласса» после импорта проекта в Eclipse
Использование контекста в фрагменте
Как заставить Android-устройство вибрировать с разной частотой?
Room - Директория экспорта схемы не указана аннотационному процессору, не удается экспортировать схему