关于Java:使用Dagger 2进行Android单元测试

Android Unit Tests with Dagger 2

我有一个使用Dagger 2进行依赖注入的Android应用。 我还在使用最新的gradle构建工具,该工具允许对单元测试和工具测试使用一个构建变体。 我在我的应用程序中使用java.util.Random,我想对此进行模拟以进行测试。 我正在测试的类不使用任何Android东西,因此它们只是常规的Java类。

在我的主代码中,我在扩展Application类的类中定义了Component,但是在单元测试中,我没有使用Application。 我尝试定义测试ModuleComponent,但是Dagger不会生成Component。 我还尝试使用在应用程序中定义的Component并在构建它时交换Module,但是应用程序的Component没有用于我的测试类的inject方法。 如何提供Random的模拟实现以进行测试?

这是一些示例代码:

应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
    }
}

模块:

1
2
3
4
5
6
7
8
9
@Module
public class PipeGameModule {

    @Provides
    @Singleton
    Random provideRandom() {
        return new Random();
    }
}

测试的基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(); // Doesn't work
    }

    public PipeGameTest component() {
        return pipeGameTest;
    }
}

要么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseModelTest {

    PipeGameApplication.PipeGame pipeGameTest;

    // This works if I make the test module extend
    // the prod module, but it can't inject my test classes
    @Before
    public void setUp() {
        pipeGameTest = DaggerPipeGameApplication_PipeGame.builder().pipeGameModule(new PipeGameModuleTest()).build();
    }

    public PipeGameApplication.PipeGame component() {
        return pipeGameTest;
    }
}

测试模块:

1
2
3
4
5
6
7
8
9
@Module
public class PipeGameTestModule {

    @Provides
    @Singleton
    Random provideRandom() {
        return mock(Random.class);
    }
}


如果没有一些解决方法,当前使用Dagger 2(从v2.0.0开始)是不可能的。你可以在这里读到它。

有关可能的解决方法的更多信息:

  • 如何在Dagger 2.0的单元测试中覆盖模块/依赖关系?

  • 使用Dagger2时创建测试依赖项


您已经说过了:

application's Component doesn't have inject methods for my test classes

因此,要解决此问题,我们可以对您的Application类进行测试。然后,我们可以为您的模块提供测试版本。为了使所有这些都能在测试中运行,我们可以使用Robolectric。

1)创建您的Application类的测试版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
    }}

2)您原始的Application类需要具有initComponent()和pipeGameModule()方法

1
2
3
4
5
6
7
8
9
10
public class PipeGameApp extends Application {
    protected void initComponent() {
        DaggerPipeGameComponent.builder()
            .pipeGameModule(getPipeGameModule())
            .build();
    }

    protected PipeGameModule pipeGameModule() {
        return new PipeGameModule(this);
    }}

3)您的PipeGameTestModule应该使用构造函数扩展生产模块:

1
2
3
4
public class PipeGameTestModule extends PipeGameModule {
    public PipeGameTestModule(Application app) {
        super(app);
    }}

4)现在,在junit test的setup()方法中,在测试应用程序上设置以下测试模块:

1
2
3
4
5
6
@Before
public void setup() {
    TestPipeGameApp app = (TestPipeGameApp) RuntimeEnvironment.application;
    PipeGameTestModule module = new PipeGameTestModule(app);
    app.setPipeGameModule(module);
}

现在,您可以按自己的意愿自定义测试模块。


我认为您可以通过从另一个角度看待这个问题。通过将Dock的模拟依赖项注入到受测试的构造类中,您可以轻松地对Dagger进行单元测试。

我的意思是说,在测试设置中,您可以:

  • 模拟被测类的依赖
  • 使用模拟的依赖项手动构造要测试的类

我们不需要测试依赖项是否正确注入,因为Dagger在编译过程中会验证依赖关系图的正确性。因此,编译失败将报告任何此类错误。这就是为什么可以接受在setup方法中手动创建被测类的原因。

在受测类中使用构造函数注入依赖项的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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由框架构造):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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() {}

  ...
}


如果您将dagger2与Android一起使用,则可以使用应用程序风味来提供模拟资源。

有关模拟测试中的口味演示,请参见此处(无匕首):

该代码库有一个示例:
https://github.com/googlecodelabs/android-testing

在您的/src/prod/com/yourcompany/Component.java中
您提供生产组件。

在您的/src/mock/com/yourcompany/Component.java中
您提供了模拟组件。

这使您可以创建带有或不带有模拟的应用程序构建。
它还允许并行开发(一个团队的后端,另一个团队的前端应用程序),您可以进行模拟,直到api方法可用为止。

gradle命令的外观(其Makefile):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
install_mock:
    ./gradlew installMockDebug

install:
    ./gradlew installProdDebug

test_unit:
    ./gradlew testMockDebugUnitTest

test_integration_mock:
    ./gradlew connectedMockDebugAndroidTest

test_integration_prod:
    ./gradlew connectedProdDebugAndroidTest


实际上,我遇到了同样的问题,找到了一个非常简单的解决方案。
我认为这不是最好的解决方案,但可以解决您的问题。

在您的应用模块中创建一个类似的类:

1
2
3
4
5
public class ActivityTest<T extends ViewModelBase> {

    @Inject
    public T vm;
}

然后,在您的AppComponent中添加:

1
void inject(ActivityTest<LoginFragmentVM> activityTest);

然后,您可以将其注入您的测试课程中。

1
2
3
4
5
6
7
8
9
10
11
12
 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();
    }
}