Skip to main content

android-integration-tests

title: IntegrationTesting Guide sidebar_label: Integration Testing

Integration testing focuses on verifying the interactions between different components or modules within the application. Unlike end-to-end testing, which tests the entire flow from the user interface to the backend, integration testing is more focused and tests how specific parts of the system work together. This is particularly useful in identifying issues in the way modules communicate with each other, ensuring that data is correctly passed and processed across the system.

Steps to Create an Integration Test

  1. Create a Class That Inherits BaseIntegrationTest: Start by creating a new test class that extends BaseIntegrationTest. This base class provides the necessary setup for mocking dependencies and managing test-specific configurations.
  2. Mock the Different APIs: Use mocking frameworks like Mockito to mock the APIs or external services your components rely on. This isolates the components under test, allowing you to focus on their interactions without involving actual network calls or external dependencies.
  3. Navigate to first page under testing
  4. Utilize Helper Classes: Just like in end-to-end testing, helper classes can simplify interactions with the components under test. These helpers abstract away complex operations, making your test code cleaner and easier to maintain.
  5. Write the Test: Write the actual test methods within your class. Use the provided helpers and the setup environment to simulate interactions between components and verify the expected behaviors.

BaseIntegrationTest

Key Components

- Thread Dispatchers: Ensures that all jobs on the main, IO, and default threads are completed before interacting with the UI.
- Navigation Controller: Sets up a TestNavHostController with custom navigators (Fragment, DialogFragment, Activity) to simulate navigation scenarios during tests.
- Dependency Injection: Integrates Hilt for injecting necessary dependencies, ensuring components like mock Api classes are available during tests.

Usage :

@HiltAndroidTest
@UninstallModules(
UnauthenticatedClientModule::class,
)
class SignupIntegrationTest : BaseIntegrationTest() {

@UninstallModules(UnauthenticatedClientModule::class):

This annotation is used to uninstall specific Hilt modules for the duration of the test. In this case, the UnauthenticatedClientModule is uninstalled, meaning that any bindings provided by this module will be removed from the dependency graph during the test. This must be done when you want to replace certain dependencies with test-specific implementations or mocks.

Mock the different APIs

@HiltAndroidTest
@UninstallModules(
UnauthenticatedClientModule::class,
)
class SignupIntegrationTest : BaseIntegrationTest() {


@BindValue
@JvmField
val unauthenticatedClientApi: UnauthenticatedClientApi = mockk(relaxed = true)

This code is used within a Hilt-based test to bind a mocked version of UnauthenticatedClientApi to the Hilt dependency graph. The @BindValue annotation ensures that this mock replaces the real implementation during testing, allowing you to control how the UnauthenticatedClientApi behaves in your tests. The mockk(relaxed = true) creates a mock that automatically handles undefined interactions, simplifying test setup. So you can now define a response for this Api :

  coEvery { unauthenticatedClientApi.checkEmail("[email protected]") } returns Response.error(
400,
"{\"code\":400,\"detail\":\"an error occurred\",\"id\":\"client\",\"status\":\"Bad Request\"}\n".toResponseBody(),
)

All integration tests rely on mock APIs. To avoid adding the annotation manually to every test, global Hilt modules are defined in :test:integration .../test.integration.di.

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [OffersApiModule::class],
)
object TestOffersApiModule {
@Provides
@Singleton
fun provideOffersApi(): OffersApi = mockk()
}

This module replaces OffersApiModule with TestOffersApiModule, providing a mockk instance instead of a real implementation. This ensures that no actual API calls are made during testing.

To specify the mock's behavior, inject it into your test using @Inject:

@Inject
lateinit var offersApi: OffersApi

coEvery { offersApi.postSignUpOffers() } returns response

Navigate to first page under testing

In the context of testing, especially when dealing with integration tests that cannot rely on React Native, it's necessary to directly land on a native page of the app. This ensures that the tests are independent of any React Native components, focusing solely on the native Android side.

The app is structured with several subgraphs, each representing a different section of the app:

- Home
- Racing
- Sports
- Next Up
- More

To navigate to a specific page during testing, we use the launchActivity function.

launchActivity(ParentRoute.SignUp.pattern, ModalRoute.SignUpWelcome.pattern)

This function requires two parameters:

1. Subgraph Route: The first parameter is the route of the parent subgraph. This defines the general area of the app you're targeting (e.g., Home, Racing).

2. Page Route: The second parameter is the specific page within the subgraph that you want to test. This points directly to the screen you wish to load.

These routes are defined in the NavController.createAppGraph function, which maps out all the pages and their respective routes within each subgraph. By passing these two parameters to launchActivity, you instruct the app to start at the specified page within the chosen subgraph, allowing you to test that page's functionality in isolation.

Some tests may require a custom navigation graph. You can define it by passing a parameter to launchActivity:

launchActivity {
navController.createGraph(startDestination = ModalRoute.ForgotPassword.pattern) {
dialog<PasswordRecoveryInputDialogFragment>(ModalRoute.ForgotPassword.pattern)
dialog<PasswordRecoveryConfirmationDialogFragment>(ModalRoute.ForgotPasswordConfirmation.pattern) {
argument(NavArgs.ForgotPasswordConfirmation.IS_EMAIL) {
type = NavType.BoolType
}
}
}
}

This allows you to set up a specific navigation flow for your test scenario.

Location

Integration tests must be placed in a designated location to ensure they run only when the associated feature module is modified.

For example, a new test for the "more-menu" should be stored in: test:integration:com.entaingroup.mobile.test.integration.tests.more.menu

Utilizing a Helper Class

Purpose of a Helper Class

The main goal of a helper class in end-to-end testing is to abstract the use of either the Compose matcher or the Espresso matcher, allowing tests to work seamlessly regardless of the underlying UI framework. This abstraction ensures that when a page is migrated from traditional views to Jetpack Compose, the only necessary change is in the helper class itself, not the test cases. This approach promotes stability and reduces the need for extensive test refactoring.

Example

Here’s an example of how a helper class works:

internal class SignUpPageHelper<T>(private val matcher: BaseMatcher<T>) {
internal fun checkEmail(email: String) {
with(matcher) {
withTag(EMAIL_TEXT).checkHasText(email)
}
}
}

Accessing a Helper Class

To use a helper class, you typically retrieve it from the PageHelper:

class PageHelper @Inject constructor(
private val featureFlag: FeatureFlag,
private val composeMatcher: ComposeMatcher,
private val espressoViewMatcher: EspressoViewMatcher,
) {
internal fun onSignUpPage(block: SignUpPageHelper<*>.() -> Unit) {
val matcher =
if (featureFlag.getFlag(FeatureFlagKey.SIGN_UP_NATIVE)) composeMatcher else espressoViewMatcher
SignUpPageHelper(matcher).block()
}
}

Here, the PageHelper class decides which matcher to use based on a feature flag. For example, if the SIGN_UP_NATIVE feature flag is enabled, the composeMatcher is used; otherwise, the espressoViewMatcher is chosen. This ensures that whether the page is implemented using traditional views or Jetpack Compose, the same test logic can be applied without modification once a page is migrated.

Usage :

@HiltAndroidTest
class SignUpTest : BaseEndToEndTest() {

@Test
fun signUpVerified() {
pageHelper.onAppBar {
clickOnJoin()
}

Region-Specific Testing

Some tests are designed for the NZ variant, while others apply only to AU. To restrict a test to a specific region, use:

@get:Rule
val regionTestRule = RegionTestRule()

Then, annotate your tests with either:

@Region(TestRegion.AU)

or

@Region(TestRegion.NZ)