0

Юнит-тесты Android с Dagger 2

3

Проблема с мокированием 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 ответ(ов)

0

Вы действительно правы, когда говорите, что

компонент вашего приложения не имеет методов для внедрения в мои классы тестов.

Чтобы обойти эту проблему, мы можем создать тестовую версию вашего класса Application, а затем сделать тестовую версию вашего модуля. Для того чтобы всё это работало в тестах, мы можем использовать Robolectric.

  1. Создайте тестовую версию вашего 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();
    }
}
  1. **Ваш оригинальный класс Application должен содержать методы initComponent() и pipeGameModule():
public class PipeGameApp extends Application {
    protected void initComponent() {
        DaggerPipeGameComponent.builder()
            .pipeGameModule(getPipeGameModule())
            .build();
    }

    protected PipeGameModule pipeGameModule() {
        return new PipeGameModule(this);
    }
}
  1. Ваш PipeGameTestModule должен расширять производственный модуль с конструктором:
public class PipeGameTestModule extends PipeGameModule {
    public PipeGameTestModule(Application app) {
        super(app);
    }
}
  1. Теперь в методе setup() вашего junit теста установите этот тестовый модуль в вашем тестовом приложении:
@Before
public void setup() {
    TestPipeGameApp app = (TestPipeGameApp) RuntimeEnvironment.application;
    PipeGameTestModule module = new PipeGameTestModule(app);
    app.setPipeGameModule(module);
}

Теперь вы можете настроить ваш тестовый модуль так, как вы изначально хотели.

0

В вашем случае, я считаю, что можно подойти к решению этой задачи с другой стороны. Вы сможете легко протестировать свой класс, не полагаясь на 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, что значительно упростит процесс тестирования.

0

Если вы используете 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

Таким образом, вы можете удобно управлять разными версиями вашего приложения, что значительно упрощает процесс тестирования и разработки.

0

У меня была та же проблема, и я нашел очень простое решение. Хотя это, возможно, не самое лучшее решение, оно определенно поможет вам.

Создайте аналогичный класс в вашем модуле приложения:

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 в тестах, как вам нужно.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь