Skip to content

Commit

Permalink
Merge pull request #146 from appunite/end-to-end-test
Browse files Browse the repository at this point in the history
LD-125 Add End-to-end tests
  • Loading branch information
jacek-marchwicki authored Oct 19, 2023
2 parents 258d9ae + 6d085fa commit 0848e01
Show file tree
Hide file tree
Showing 19 changed files with 746 additions and 51 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/run-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ jobs:

- name: Run test
run: ./gradlew test
env:
LOUDIUS_CLIENT_SECRET: ${{ secrets.LOUDIUS_CLIENT_SECRET }}
LOUDIUS_CLIENT_ID: ${{ secrets.LOUDIUS_CLIENT_ID }}
LOUDIUS_GITHUB_USER_PASSWORD: ${{ secrets.LOUDIUS_GITHUB_USER_PASSWORD }}
LOUDIUS_GITHUB_USER_NAME: ${{ secrets.LOUDIUS_GITHUB_USER_NAME }}
LOUDIUS_GITHUB_USER_OTP_SECRET: ${{ secrets.LOUDIUS_GITHUB_USER_OTP_SECRET }}

- name: Upload tests results
if: always()
Expand Down Expand Up @@ -66,6 +72,12 @@ jobs:

- name: Assemble App Debug APK and Android Instrumentation Tests
run: ./gradlew assembleDebug assembleDebugAndroidTest
env:
LOUDIUS_CLIENT_SECRET: ${{ secrets.LOUDIUS_CLIENT_SECRET }}
LOUDIUS_CLIENT_ID: ${{ secrets.LOUDIUS_CLIENT_ID }}
LOUDIUS_GITHUB_USER_PASSWORD: ${{ secrets.LOUDIUS_GITHUB_USER_PASSWORD }}
LOUDIUS_GITHUB_USER_NAME: ${{ secrets.LOUDIUS_GITHUB_USER_NAME }}
LOUDIUS_GITHUB_USER_OTP_SECRET: ${{ secrets.LOUDIUS_GITHUB_USER_OTP_SECRET }}

- id: auth
name: Authenticate to Google Cloud
Expand Down
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,33 @@ Here's an example of an experiment that meets our rules:

## 🚀 Project setup

In order to properly start the application and use it, the CLIENT_SECRET environment variable must
be set on your computer. CLIENT_SECRET is a GitHub client secret key provided
from ``Settings -> Developer Settings -> OAuth Apps -> my application``.
In order to properly start the application and use it, the `LOUDIUS_CLIENT_SECRET` and
`LOUDIUS_CLIENT_ID` environment variables must be set on your computer.

If you're AppUniter, you can find this secrets [here](https://www.notion.so/appunite/Github-Secrets-0c2c6c1b56e2472c8a4752241f1e20d3?pvs=4).
* `LOUDIUS_CLIENT_SECRET` is a GitHub client secret key
* `LOUDIUS_CLIENT_ID` is a GitHub client id
*
both are provided from ``Settings -> Developer Settings -> OAuth Apps -> my application``.

If you're not, don't worry, here's a video to help you create a new one:
If you're not AppUniter, here's a video to help you create such appliation:

<https://github.com/appunite/Loudius/assets/72873966/4820b6df-81ca-48ed-9f3c-425011b758dd>
<https://github.com/appunite/Loudius/assets/72873966/4820b6df-81ca-48ed-9f3c-425011b758dd>.

If you'd like to run end-to-end tests you'd also need `LOUDIUS_GITHUB_USER_NAME` and
`LOUDIUS_GITHUB_USER_PASSWORD` which are credentials to GitHub test account.
This is just a standard GitHub account that you can create by yourself.

If you're AppUniter, you can find those secrets [here](https://www.notion.so/appunite/Github-Secrets-0c2c6c1b56e2472c8a4752241f1e20d3?pvs=4).

### How to set environmental variable on mac?

1. Launch zsh (command `zsh`)
2. `$ echo 'export CLIENT_SECRET=you know what' >> ~/.zshenv`
3. Restart Android studio and Terminal.
4. `$ echo $CLIENT_SECRET`
2. `$ echo 'export LOUDIUS_CLIENT_SECRET="you know what"' >> ~/.zshenv`
3. `$ echo 'export LOUDIUS_CLIENT_ID="you know what"' >> ~/.zshenv`
4. optionally: `$ echo 'export LOUDIUS_GITHUB_USER_NAME="you know what"' >> ~/.zshenv`
5. optionally: `$ echo 'export LOUDIUS_GITHUB_USER_PASSWORD="you know what"' >> ~/.zshenv`
6. Restart Android studio and Terminal.
7. `$ echo $LOUDIUS_CLIENT_ID/$LOUDIUS_CLIENT_SECRET $LOUDIUS_GITHUB_USER_NAME/$LOUDIUS_GITHUB_USER_PASSWORD`

### Screenshots tests

Expand Down
13 changes: 13 additions & 0 deletions app-shared-tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ android {
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
if (System.env.LOUDIUS_GITHUB_USER_PASSWORD == null || System.env.LOUDIUS_GITHUB_USER_PASSWORD.isEmpty()) {
logger.warn("You need to set LOUDIUS_GITHUB_USER_PASSWORD in your environment variables")
}
buildConfigField "String", "LOUDIUS_GITHUB_USER_PASSWORD", "\"${System.env.LOUDIUS_GITHUB_USER_PASSWORD}\""
if (System.env.LOUDIUS_GITHUB_USER_NAME == null || System.env.LOUDIUS_GITHUB_USER_NAME.isEmpty()) {
logger.warn("You need to set LOUDIUS_GITHUB_USER_NAME in your environment variables")
}
buildConfigField "String", "LOUDIUS_GITHUB_USER_NAME", "\"${System.env.LOUDIUS_GITHUB_USER_NAME}\""
if (System.env.LOUDIUS_GITHUB_USER_OTP_SECRET == null || System.env.LOUDIUS_GITHUB_USER_OTP_SECRET.isEmpty()) {
logger.warn("You need to set LOUDIUS_GITHUB_USER_OTP_SECRET in your environment variables")
}
buildConfigField "String", "LOUDIUS_GITHUB_USER_OTP_SECRET", "\"${System.env.LOUDIUS_GITHUB_USER_OTP_SECRET}\""
}
buildFeatures {
compose true
Expand Down Expand Up @@ -92,6 +104,7 @@ dependencies {

// Firebase instrumentation lib
api 'com.google.firebase:testlab-instr-lib:0.2'
api 'dev.samstevens.totp:totp:1.7.1'

// ktlint
ktlintRuleset project(":custom-ktlint-rules")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.ui.test.onNodeWithText
import com.appunite.loudius.components.theme.LoudiusTheme
import com.appunite.loudius.ui.pullrequests.PullRequestsScreen
import com.appunite.loudius.util.IntegrationTestRule
import com.appunite.loudius.util.MockWebServerRule
import com.appunite.loudius.util.Register
import com.appunite.loudius.util.waitUntilLoadingDoesNotExist
import org.junit.Before
Expand All @@ -29,9 +30,12 @@ import org.junit.Test

abstract class AbsPullRequestsScreenTest {

@get:Rule
@get:Rule(order = 0)
val integrationTestRule by lazy { IntegrationTestRule(this) }

@get:Rule(order = 1)
var mockWebServer: MockWebServerRule = MockWebServerRule()

@Before
fun setUp() {
integrationTestRule.setUp()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.ui.test.performClick
import com.appunite.loudius.components.theme.LoudiusTheme
import com.appunite.loudius.ui.reviewers.ReviewersScreen
import com.appunite.loudius.util.IntegrationTestRule
import com.appunite.loudius.util.MockWebServerRule
import com.appunite.loudius.util.Register
import com.appunite.loudius.util.waitUntilLoadingDoesNotExist
import org.junit.Before
Expand All @@ -30,9 +31,12 @@ import org.junit.Test

abstract class AbsReviewersScreenTest {

@get:Rule
@get:Rule(order = 0)
val integrationTestRule by lazy { IntegrationTestRule(this) }

@get:Rule(order = 1)
var mockWebServer: MockWebServerRule = MockWebServerRule()

@Before
fun setUp() {
integrationTestRule.initTests()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,25 @@ import android.app.Activity
import android.app.Instrumentation
import android.content.Intent
import android.net.Uri
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.rule.IntentsRule
import com.appunite.loudius.util.IntegrationTestRule
import com.appunite.loudius.util.MockWebServerRule
import com.appunite.loudius.util.Register
import com.appunite.loudius.util.waitUntilLoadingDoesNotExist
import org.junit.Before
import org.junit.Rule
import org.junit.Test

abstract class AbsWalkThroughAppTest {

@get:Rule(order = 0)
val integrationTestRule by lazy { IntegrationTestRule(this, MainActivity::class.java) }
abstract class AbsWalkThroughAppTest : UniversalWalkThroughAppTest() {

@get:Rule(order = 1)
var mockWebServer: MockWebServerRule = MockWebServerRule()

@get:Rule(order = 2)
val intents = IntentsRule()

@Before
fun setUp() {
integrationTestRule.setUp()
}

@Test
fun whenLoginScreenIsVisible_LoginButtonOpensGithubAuth(): Unit = with(integrationTestRule) {
fun prepareMocks() {
Register.run {
user(mockWebServer)
accessToken(mockWebServer)
Expand All @@ -62,31 +52,17 @@ abstract class AbsWalkThroughAppTest {
.respondWithFunction {
Instrumentation.ActivityResult(Activity.RESULT_OK, null)
}
}

composeTestRule.onNodeWithText("Log in").performClick()

override fun performGitHubLogin() {
// simulate opening a deeplink
ActivityScenario.launch<MainActivity>(
Intent(
Intent.ACTION_VIEW,
Uri.parse("loudius://callback?code=example_code"),
).apply {
setPackage(composeTestRule.activity.packageName)
setPackage(integrationTestRule.composeTestRule.activity.packageName)
},
)

composeTestRule.waitUntilLoadingDoesNotExist()

composeTestRule.onNodeWithText("First Pull-Request title").performClick()

composeTestRule.waitUntilLoadingDoesNotExist()

composeTestRule.onNodeWithText("Notify").performClick()

composeTestRule.waitUntilLoadingDoesNotExist()

composeTestRule
.onNodeWithText("Awesome! Your collaborator have been pinged for some serious code review action! \uD83C\uDF89")
.assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2023 AppUnite S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.appunite.loudius

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.appunite.loudius.util.IntegrationTestRule
import com.appunite.loudius.util.waitUntilLoadingDoesNotExist
import org.junit.Before
import org.junit.Rule
import org.junit.Test

abstract class UniversalWalkThroughAppTest {

@get:Rule(order = 0)
val integrationTestRule by lazy { IntegrationTestRule(this, MainActivity::class.java) }

@Before
fun setUp() {
integrationTestRule.setUp()
}

@Test
fun whenLoginScreenIsVisible_LoginButtonOpensGithubAuth(): Unit = with(integrationTestRule) {
composeTestRule.onNodeWithText("Log in").performClick()

performGitHubLogin()

composeTestRule.waitUntilLoadingDoesNotExist()

composeTestRule.onNodeWithText("First Pull-Request title").performClick()

composeTestRule.waitUntilLoadingDoesNotExist()

composeTestRule.onNodeWithText("Notify").performClick()

composeTestRule.waitUntilLoadingDoesNotExist()

composeTestRule
.onNodeWithText("Awesome! Your collaborator have been pinged for some serious code review action! \uD83C\uDF89")
.assertIsDisplayed()
}

abstract fun performGitHubLogin()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2023 AppUnite S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.appunite.loudius.util

/**
* Adds description to a test, so failures are better recorded
*
* Example usage:
* ```kt
* description("Log-in") {
* description("Fill e-mail and password") {
* onView(withHint("E-mail")).perform(type("[email protected]"))
* onView(withHint("Password")).perform(type("password"))
* }
* description("Submit") {
* onView(withId(R.id.login_button)).perform(click())
* }
* }
* description("Ensure home screen is displayed") {
* onView(withClass("com.example.com.LoginScreen")).check(isDisplayed())
* }
* ```
*
* In case of failure, you'll see:
*
* ```
* Exception thrown DescriptionAssertionError("Error in step: Log-in -> Fill e-mail and password")
* ```
*
* The exception is always thrown with the root cause attached.
*/
fun <T> description(description: String, lambda: () -> T): T {
try {
return lambda()
} catch (error: DescriptionAssertionError) {
throw DescriptionAssertionError("$description -> ${error.step}", error.cause!!)
} catch (error: AssertionError) {
throw DescriptionAssertionError(description, error)
}
}

class DescriptionAssertionError(val step: String, cause: Throwable) :
AssertionError("Error in step: \"$step\"", cause)
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ class IntegrationTestRule(
testActivity: Class<out ComponentActivity> = TestActivity::class.java,
) : TestRule {

val mockWebServer = MockWebServerRule()
val composeTestRule = createAndroidComposeRule(testActivity).apply {
registerIdlingResource(countingResource.toIdlingResource())
}
private val hiltRule = HiltAndroidRule(testClass)
private val screenshotTestRule = ScreenshotTestRule()

override fun apply(base: Statement, description: Description): Statement {
return RuleChain.outerRule(mockWebServer)
return RuleChain.emptyRuleChain()
.around(hiltRule)
.around(composeTestRule)
.around(screenshotTestRule)
Expand Down
Loading

0 comments on commit 0848e01

Please sign in to comment.