使用Dagger 2进行Android单元测试

57
我有一个使用Dagger 2进行依赖注入的Android应用程序。我还使用了最新的gradle构建工具,允许为单元测试和仪表盘测试创建不同的构建变体。我在我的应用程序中使用了java.util.Random,并希望在测试中对其进行模拟。我正在测试的类不使用任何Android内容,因此它们只是普通的Java类。
在我的主要代码中,我在扩展Application类的类中定义了一个Component,但在单元测试中我没有使用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(); // Doesn't work
    }

    public PipeGameTest component() {
        return pipeGameTest;
    }
}

或者:

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;
    }
}

测试模块:

@Module
public class PipeGameTestModule {

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

这将有所帮助 https://dev59.com/xF8d5IYBdhLWcg3wVQ-9?lq=1 - Narayana Sai
你可能需要考虑到Dagger在单元测试中并不被推荐使用:https://google.github.io/dagger/testing.html - IgorGanapolsky
5个回答

32

嗨@tomrozb。你能否提供关于这个问题的最新答案或文章建议?谢谢。 - Chisko

7
你的说法一针见血:应用程序组件没有为我的测试类注入方法。
因此,为了解决这个问题,我们可以创建一个应用程序类的测试版本。然后我们可以创建一个模块的测试版本。为了在测试中运行所有内容,我们可以使用Robolectric。
1)创建应用程序类的测试版本。
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) 您原始的应用程序类需要具有initComponent()pipeGameModule()方法。

public class PipeGameApp extends Application {
    protected void initComponent() {
        DaggerPipeGameComponent.builder()
            .pipeGameModule(getPipeGameModule())
            .build();
    }

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

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

public class PipeGameTestModule extends PipeGameModule {
    public PipeGameTestModule(Application app) {
        super(app);
    }}

4) 现在,在您的Junit测试的setup()方法中,将此测试模块设置为您的测试应用程序:

@Before
public void setup() {
    TestPipeGameApp app = (TestPipeGameApp) RuntimeEnvironment.application;
    PipeGameTestModule module = new PipeGameTestModule(app);
    app.setPipeGameModule(module);
}

现在,您可以按照最初的想法自定义您的测试模块。

1
“Runtime Environment” 是什么意思? - superuserdo
太好了,Igor,谢谢。我只需要一个信息:这个是如何注入测试类实例的? - Benoit Duffez
没关系,我只是需要在我的组件中声明一个新的 inject 方法,并从我的测试的 @Before 方法中调用它。抱歉问个愚蠢的问题! :) - Benoit Duffez

2
在我看来,您可以从不同的角度来解决这个问题。您可以通过不依赖Dagger来构造测试类,并将其注入到模拟的依赖项中,从而轻松地对其进行单元测试。
我的意思是,在测试设置中,您可以:
- 模拟测试类的依赖项 - 手动使用模拟的依赖项构造测试类
我们不需要测试依赖项是否正确注入,因为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() {}

  ...
}

这在我的情况下并不适用,因为我目前注入了很多依赖项,但对于更简单的情况,这肯定会起作用,如果您能以这种方式完成,我绝对认为这是正确的方法。 - Pikaling
如果你没有使用Dagger,为什么要建议使用 @Inject Random random; 呢? - IgorGanapolsky

1
如果您正在使用dagger2和Android,可以使用应用程序flavours来提供模拟资源。
在这里查看一个演示flavours在模拟测试中的示例(不使用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

1
那个仓库没有展示Dagger 2组件。你在项目中是如何模拟它们的? - IgorGanapolsky

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();
    }
}

什么是 ViewModelBase - IgorGanapolsky

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接