Skip to content

Commit

Permalink
Instrumentation tests (#41)
Browse files Browse the repository at this point in the history
# *Instrumentation tests*

- Add `HiltApplicationTestRunner` to execute instrumented android tests
with hilt
- Introduce `ComposeContentActivity` in `design` module, in order to be
able to start single screen instrumented tests in non-app modules - see
`OnboardingScreenTest.kt` in `onboarding` module for example usage
- Introduce test identifier (test tags) on relevant compose nodes of the
screens in order to reference in UI tests
- Introduce some extension helper functions on `ComposeTestRule` and
`Modifier`
- Add `OnboardingScreenTest` in onboarding module using fake repository
(via a test hilt module) and screen display and click interaction
- `Simulator` - Introduced the concept of simulators. Wrappers around
`ComposeTestRule` with a DSL-like API to interact with a certain
composable screen.
- `app`
- `AppNavigationTest` - Several test methods that invoke navigation via
`Navigator` component and ensure display of the expected screen
- `OnboardingFlowTest` tests the display of the initial onboarding
screens and the navigation to the next expected one
- Included several simulators that can also be reused in future tests -
We can think of extracting all the simulators in a separate module for
reusability among modules

TODO:
- Add Github action to run `connectedAndroidTest` (issue #21) in CI and
include in codecov

## ♻️ Current situation & Problem
*Link any open issues or pull requests (PRs) related to this PR. Please
ensure that all non-trivial PRs are first tracked and discussed in an
existing GitHub issue or discussion.*


## ⚙️ Release Notes 
*Add a bullet point list summary of the feature and possible migration
guides if this is a breaking change so this section can be added to the
release notes.*
*Include code snippets that provide examples of the feature implemented
or links to the documentation if it appends or changes the public
interface.*


## 📚 Documentation
*Please ensure that you properly document any additions in conformance
to [Spezi Documentation
Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).*
*You can use this section to describe your solution, but we encourage
contributors to document your reasoning and changes using in-line
documentation.*


## ✅ Testing
*Please ensure that the PR meets the testing requirements set by CodeCov
and that new functionality is appropriately tested.*
*This section describes important information about the tests and why
some elements might not be testable.*


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [ ] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).

Co-authored-by: Paul Schmiedmayer <[email protected]>
  • Loading branch information
eldcn and PSchmiedmayer authored Jun 19, 2024
1 parent b9f4ae6 commit 9965c5a
Show file tree
Hide file tree
Showing 39 changed files with 1,026 additions and 80 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ replay_pid*

# IDEA/Android Studio project files
*.iml
.idea/*
.idea/
**/.idea/
!.idea/copyright
# Keep the code styles.
!/.idea/codeStyles
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package edu.stanford.bdh.engagehf

import android.Manifest
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import edu.stanford.bdh.engagehf.simulator.NavigatorSimulator
import edu.stanford.spezi.core.navigation.Navigator
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject

@HiltAndroidTest
class AppNavigationTest {

@get:Rule(order = 1)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>()

@get:Rule
val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
)

@Inject
lateinit var navigator: Navigator

@Before
fun init() {
hiltRule.inject()
}

@Test
fun `test start destination`() {
mainActivity {
assertOnboardingIsDisplayed()
}
}

@Test
fun `test navigation to onboarding screen`() {
mainActivity {
navigateToOnboardingScreen()
assertOnboardingIsDisplayed()
}
}

@Test
fun `test navigation to bluetooth screen`() {
mainActivity {
navigateToBluetoothScreen()
assertBluetoothScreenIsDisplayed()
}
}

@Test
fun `test navigation to login screen`() {
mainActivity {
navigateToLoginScreen()
assertLoginScreenIsDisplayed()
}
}

@Test
fun `test navigation to register screen`() {
mainActivity {
navigateToRegisterScreen()
assertRegisterScreenIsDisplayed()
}
}

@Test
fun `test navigation to invitation code screen`() {
mainActivity {
navigateToInvitationCodeScreen()
assertInvitationCodeScreenIsDisplayed()
}
}

@Test
fun `test navigation to sequential onboarding screen`() {
mainActivity {
navigateToSequentialOnboardingScreen()
assertSequentialOnboardingScreenIsDisplayed()
}
}

@Test
fun `test navigation to consent screen`() {
mainActivity {
navigateToConsentScreen()
assertConsentScreenIsDisplayed()
}
}

private fun mainActivity(scope: NavigatorSimulator.() -> Unit) {
NavigatorSimulator(composeTestRule, navigator).apply(scope)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package edu.stanford.bdh.engagehf

import androidx.compose.ui.test.junit4.createAndroidComposeRule
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import edu.stanford.bdh.engagehf.simulator.OnboardingFlowSimulator
import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeRepository
import edu.stanford.spezi.module.onboarding.onboarding.OnboardingRepository
import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingRepository
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject

@HiltAndroidTest
class OnboardingFlowTest {

@get:Rule(order = 1)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 2)
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Inject
lateinit var onboardingRepository: OnboardingRepository

@Inject
lateinit var sequentialOnboardingRepository: SequentialOnboardingRepository

@Inject
lateinit var invitationCodeRepository: InvitationCodeRepository

@Before
fun init() {
hiltRule.inject()
}

@Test
fun `it should show expected state of initial onboarding screen`() = runTest {
val onboardingData = onboardingRepository.getOnboardingData().getOrThrow()
onboardingFlow {
onboardingScreen {
assertDisplayed()
assertTitle(text = onboardingData.title)
assertSubtitle(text = onboardingData.subTitle)
assertContinueButtonTitle(text = onboardingData.continueButtonText)

onAreasList {
assertDisplayed()
onboardingData.areas.forEach {
assertAreaTitle(title = it.title)
}
}
}
}
}

@Test
fun `it should navigate and display sequential onboarding correctly`() = runTest {
val stepTitle = sequentialOnboardingRepository.getSequentialOnboardingData().steps.first().title
onboardingFlow {
onboardingScreen {
clickContinueButton()
}
sequentialOnboarding {
assertIsDisplayed()
assertPagerIsDisplayed()
assertPageIndicatorIsDisplayed()
assertPageTitle(text = stepTitle)
}
}
}

@Test
fun `it should display and navigate invitation screen correctly`() = runTest {
val steps = sequentialOnboardingRepository.getSequentialOnboardingData().steps
onboardingFlow {
onboardingScreen {
clickContinueButton()
}
sequentialOnboarding {
steps.forEach {
assertPageTitle(text = it.title)
clickForward()
}
}

invitationCodeScreen {
val screenData = invitationCodeRepository.getScreenData()
assertTitle(text = screenData.title)
assertDescription(text = screenData.description)
assertIsDisplayed()
assertMainButtonDisplayed()
assertSecondaryButtonDisplayed()
}
}
}

private fun onboardingFlow(scope: OnboardingFlowSimulator.() -> Unit) {
OnboardingFlowSimulator(composeTestRule).apply(scope)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package edu.stanford.bdh.engagehf.simulator

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.ComposeTestRule
import edu.stanford.spezi.core.testing.onNodeWithIdentifier
import edu.stanford.spezi.core.testing.waitNode
import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeScreenTestIdentifier

class InvitationCodeScreenSimulator(
private val composeTestRule: ComposeTestRule,
) {
private val root = composeTestRule.onNodeWithIdentifier(InvitationCodeScreenTestIdentifier.ROOT)
private val title =
composeTestRule.onNodeWithIdentifier(InvitationCodeScreenTestIdentifier.TITLE)
private val description = composeTestRule.onNodeWithIdentifier(
InvitationCodeScreenTestIdentifier.DESCRIPTION
)
private val mainButton =
composeTestRule.onNodeWithIdentifier(InvitationCodeScreenTestIdentifier.MAIN_ACTION_BUTTON)
private val secondaryButton =
composeTestRule.onNodeWithIdentifier(InvitationCodeScreenTestIdentifier.SECONDARY_ACTION_BUTTON)

fun assertIsDisplayed() {
composeTestRule.waitNode(InvitationCodeScreenTestIdentifier.ROOT)
root.assertIsDisplayed()
}

fun assertTitle(text: String) {
title
.assertIsDisplayed()
.assertTextEquals(text)
}

fun assertDescription(text: String) {
description
.assertIsDisplayed()
.assertTextEquals(text)
}

fun assertMainButtonDisplayed() {
mainButton.assertIsDisplayed()
}

fun assertSecondaryButtonDisplayed() {
secondaryButton.assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package edu.stanford.bdh.engagehf.simulator

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import edu.stanford.bdh.engagehf.bluetooth.screen.BluetoothScreenTestIdentifier
import edu.stanford.bdh.engagehf.navigation.AppNavigationEvent
import edu.stanford.spezi.core.navigation.Navigator
import edu.stanford.spezi.core.testing.onAllNodes
import edu.stanford.spezi.core.testing.onNodeWithIdentifier
import edu.stanford.spezi.core.utils.TestIdentifier
import edu.stanford.spezi.module.account.AccountNavigationEvent
import edu.stanford.spezi.module.account.login.LoginScreenTestIdentifier
import edu.stanford.spezi.module.account.register.RegisterScreenTestIdentifier
import edu.stanford.spezi.module.onboarding.OnboardingNavigationEvent
import edu.stanford.spezi.module.onboarding.consent.ConsentScreenTestIdentifier
import edu.stanford.spezi.module.onboarding.invitation.InvitationCodeScreenTestIdentifier
import edu.stanford.spezi.module.onboarding.onboarding.OnboardingScreenTestIdentifier
import edu.stanford.spezi.module.onboarding.sequential.SequentialOnboardingScreenTestIdentifier

class NavigatorSimulator(
private val composeTestRule: ComposeTestRule,
private val navigator: Navigator,
) {
private val onboarding =
composeTestRule.onNodeWithIdentifier(OnboardingScreenTestIdentifier.ROOT)
private val register = composeTestRule.onNodeWithIdentifier(RegisterScreenTestIdentifier.ROOT)
private val login = composeTestRule.onNodeWithIdentifier(LoginScreenTestIdentifier.ROOT)
private val invitation =
composeTestRule.onNodeWithIdentifier(InvitationCodeScreenTestIdentifier.ROOT)
private val sequential =
composeTestRule.onNodeWithIdentifier(SequentialOnboardingScreenTestIdentifier.ROOT)
private val bluetooth = composeTestRule.onNodeWithIdentifier(BluetoothScreenTestIdentifier.ROOT)
private val consent = composeTestRule.onNodeWithIdentifier(ConsentScreenTestIdentifier.ROOT)

fun assertOnboardingIsDisplayed() {
waitNode(OnboardingScreenTestIdentifier.ROOT)
onboarding.assertIsDisplayed()
}

fun assertBluetoothScreenIsDisplayed() {
waitNode(BluetoothScreenTestIdentifier.ROOT)
bluetooth.assertIsDisplayed()
}

fun assertLoginScreenIsDisplayed() {
waitNode(LoginScreenTestIdentifier.ROOT)
login.assertIsDisplayed()
}

fun assertRegisterScreenIsDisplayed() {
waitNode(RegisterScreenTestIdentifier.ROOT)
register.assertIsDisplayed()
}

fun assertInvitationCodeScreenIsDisplayed() {
waitNode(InvitationCodeScreenTestIdentifier.ROOT)
invitation.assertIsDisplayed()
}

fun assertSequentialOnboardingScreenIsDisplayed() {
waitNode(SequentialOnboardingScreenTestIdentifier.ROOT)
sequential.assertIsDisplayed()
}

fun assertConsentScreenIsDisplayed() {
waitNode(ConsentScreenTestIdentifier.ROOT)
consent.assertIsDisplayed()
}

fun navigateToBluetoothScreen() {
navigator.navigateTo(AppNavigationEvent.BluetoothScreen)
}

fun navigateToOnboardingScreen() {
navigator.navigateTo(OnboardingNavigationEvent.OnboardingScreen)
}

fun navigateToLoginScreen() {
navigator.navigateTo(AccountNavigationEvent.LoginScreen(false))
}

fun navigateToRegisterScreen() {
navigator.navigateTo(AccountNavigationEvent.RegisterScreen())
}

fun navigateToInvitationCodeScreen() {
navigator.navigateTo(OnboardingNavigationEvent.InvitationCodeScreen)
}

fun navigateToSequentialOnboardingScreen() {
navigator.navigateTo(OnboardingNavigationEvent.SequentialOnboardingScreen)
}

fun navigateToConsentScreen() {
navigator.navigateTo(OnboardingNavigationEvent.ConsentScreen)
}

private fun waitNode(testIdentifier: TestIdentifier) {
composeTestRule.waitUntil {
composeTestRule.onAllNodes(testIdentifier).fetchSemanticsNodes().isNotEmpty()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package edu.stanford.bdh.engagehf.simulator

import androidx.compose.ui.test.junit4.ComposeTestRule

class OnboardingFlowSimulator(
composeTestRule: ComposeTestRule,
) {
private val onboardingScreenSimulator = OnboardingScreenSimulator(composeTestRule)
private val sequentialOnboardingScreenSimulator = SequentialOnboardingScreenSimulator(composeTestRule)
private val invitationCodeScreenSimulator = InvitationCodeScreenSimulator(composeTestRule)

fun onboardingScreen(scope: OnboardingScreenSimulator.() -> Unit) {
onboardingScreenSimulator.apply(scope)
}

fun sequentialOnboarding(scope: SequentialOnboardingScreenSimulator.() -> Unit) {
sequentialOnboardingScreenSimulator.apply(scope)
}

fun invitationCodeScreen(scope: InvitationCodeScreenSimulator.() -> Unit) {
invitationCodeScreenSimulator.apply(scope)
}
}
Loading

0 comments on commit 9965c5a

Please sign in to comment.