Issue#100 Create TestRule Saving Screenshots on UI Test failure
This commit is contained in:
parent
45bcd20b2a
commit
ca2dff2304
11 changed files with 190 additions and 7 deletions
7
.github/workflows/pull-request-jobs.yml
vendored
7
.github/workflows/pull-request-jobs.yml
vendored
|
|
@ -124,3 +124,10 @@ jobs:
|
||||||
name: Emulator-Test-Results-${{ matrix.api-level }}
|
name: Emulator-Test-Results-${{ matrix.api-level }}
|
||||||
path: ./**/build/reports/androidTests/**/*.html
|
path: ./**/build/reports/androidTests/**/*.html
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
- name: Upload Test Screenshots
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Emulator-Test-Results-${{ matrix.api-level }}
|
||||||
|
path: ./**/build/testscreenshots/*
|
||||||
|
retention-days: 1
|
||||||
|
|
@ -116,3 +116,4 @@ dependencies {
|
||||||
androidTestImplementation testFixtures(project(':core'))
|
androidTestImplementation testFixtures(project(':core'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: '../gradlescripts/pull-screenshots.gradle'
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.R
|
import org.fnives.test.showcase.R
|
||||||
|
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling
|
import org.fnives.test.showcase.android.testutil.synchronization.idlingresources.anyResourceIdling
|
||||||
import org.fnives.test.showcase.compose.screen.AppNavigation
|
import org.fnives.test.showcase.compose.screen.AppNavigation
|
||||||
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
|
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
|
||||||
|
|
@ -21,8 +22,7 @@ import org.koin.test.KoinTest
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class AuthComposeInstrumentedTest : KoinTest {
|
class AuthComposeInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@get:Rule
|
private val composeTestRule = createComposeRule()
|
||||||
val composeTestRule = createComposeRule()
|
|
||||||
private val stateRestorationTester = StateRestorationTester(composeTestRule)
|
private val stateRestorationTester = StateRestorationTester(composeTestRule)
|
||||||
|
|
||||||
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
|
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
|
||||||
|
|
@ -35,6 +35,8 @@ class AuthComposeInstrumentedTest : KoinTest {
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
||||||
.around(dispatcherTestRule)
|
.around(dispatcherTestRule)
|
||||||
|
.around(composeTestRule)
|
||||||
|
.around(ScreenshotRule("test-showcase-compose"))
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package org.fnives.test.showcase.ui.home
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
||||||
import org.fnives.test.showcase.android.testutil.activity.safeClose
|
import org.fnives.test.showcase.android.testutil.activity.safeClose
|
||||||
|
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor
|
import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor
|
||||||
import org.fnives.test.showcase.model.content.FavouriteContent
|
import org.fnives.test.showcase.model.content.FavouriteContent
|
||||||
import org.fnives.test.showcase.network.mockserver.ContentData
|
import org.fnives.test.showcase.network.mockserver.ContentData
|
||||||
|
|
@ -38,6 +40,8 @@ class MainActivityInstrumentedTest : KoinTest {
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
||||||
.around(mainDispatcherTestRule)
|
.around(mainDispatcherTestRule)
|
||||||
.around(AsyncDiffUtilInstantTestRule())
|
.around(AsyncDiffUtilInstantTestRule())
|
||||||
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
|
.around(ScreenshotRule("test-showcase"))
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
@ -48,7 +52,6 @@ class MainActivityInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
activityScenario.safeClose()
|
|
||||||
Intents.release()
|
Intents.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.R
|
import org.fnives.test.showcase.R
|
||||||
import org.fnives.test.showcase.android.testutil.activity.safeClose
|
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
||||||
|
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
import org.fnives.test.showcase.network.mockserver.scenario.auth.AuthScenario
|
||||||
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
||||||
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
|
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
|
||||||
|
|
@ -32,6 +33,8 @@ class AuthActivityInstrumentedTest : KoinTest {
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
||||||
.around(mainDispatcherTestRule)
|
.around(mainDispatcherTestRule)
|
||||||
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
|
.around(ScreenshotRule("test-showcase"))
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
@ -41,7 +44,6 @@ class AuthActivityInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
activityScenario.safeClose()
|
|
||||||
Intents.release()
|
Intents.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.intent.Intents
|
import androidx.test.espresso.intent.Intents
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.fnives.test.showcase.android.testutil.activity.safeClose
|
import org.fnives.test.showcase.android.testutil.activity.SafeCloseActivityRule
|
||||||
|
import org.fnives.test.showcase.android.testutil.screenshot.ScreenshotRule
|
||||||
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
import org.fnives.test.showcase.testutils.MockServerScenarioSetupResetingTestRule
|
||||||
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
|
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
|
||||||
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
|
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
|
||||||
|
|
@ -32,6 +33,8 @@ class SplashActivityInstrumentedTest : KoinTest {
|
||||||
@JvmField
|
@JvmField
|
||||||
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
|
||||||
.around(mainDispatcherTestRule)
|
.around(mainDispatcherTestRule)
|
||||||
|
.around(SafeCloseActivityRule { activityScenario })
|
||||||
|
.around(ScreenshotRule("test-showcase"))
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
|
@ -41,7 +44,6 @@ class SplashActivityInstrumentedTest : KoinTest {
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
activityScenario.safeClose()
|
|
||||||
Intents.release()
|
Intents.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
63
gradlescripts/pull-screenshots.gradle
Normal file
63
gradlescripts/pull-screenshots.gradle
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Variables:
|
||||||
|
// ext.screenshotsPackageName => the package name to pull from
|
||||||
|
// ext.screenshotsDirectory => the directory on the device to pull from inside the Pictures folder
|
||||||
|
// ext.screenshotsSavePath => where to pull the images
|
||||||
|
// ext.adbPath => Give adb path, if the script cant find it by itself from localProperties
|
||||||
|
|
||||||
|
def propertyOrNull = { key ->
|
||||||
|
if (extensions.extraProperties.has(key)) {
|
||||||
|
return extensions.extraProperties.get(key)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def findAdbFromLocal = {
|
||||||
|
def localProperties = new File(rootDir, "local.properties")
|
||||||
|
if (localProperties.exists()) {
|
||||||
|
Properties properties = new Properties()
|
||||||
|
localProperties.withInputStream { instr -> properties.load(instr) }
|
||||||
|
def sdkDir = properties.getProperty('sdk.dir')
|
||||||
|
return "$sdkDir/platform-tools/adb"
|
||||||
|
} else {
|
||||||
|
System.err.println("WARNING: SDK dir not found by local properties!")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task pullScreenshots(type: Exec) {
|
||||||
|
group = 'Test'
|
||||||
|
description = 'Pull screenshots'
|
||||||
|
|
||||||
|
def packageName = propertyOrNull("screenshotsPackageName") ?: "$android.defaultConfig.applicationId"
|
||||||
|
def screenshotDirectory = propertyOrNull("screenshotsDirectory") ?: "test-screenshots"
|
||||||
|
def fullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/"
|
||||||
|
def savePath = propertyOrNull("screenshotsSavePath") ?: "build/testscreenshots/"
|
||||||
|
def adb = propertyOrNull("adbPath") ?: findAdbFromLocal()
|
||||||
|
|
||||||
|
commandLine "$adb", 'pull', "$fullPath", "$savePath/"
|
||||||
|
}
|
||||||
|
|
||||||
|
task removeScreenshotsFromDevice(type: Exec) {
|
||||||
|
group = 'Test'
|
||||||
|
description = 'Delete screenshots From Device'
|
||||||
|
|
||||||
|
def packageName = propertyOrNull("screenshotsPackageName") ?: "$android.defaultConfig.applicationId"
|
||||||
|
def screenshotDirectory = propertyOrNull("screenshotsDirectory") ?: "test-screenshots"
|
||||||
|
def fullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/"
|
||||||
|
def adb = propertyOrNull("adbPath") ?: findAdbFromLocal()
|
||||||
|
|
||||||
|
commandLine "$adb", 'shell', 'rm', '-r', "$fullPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
task removeLocalScreenshots(type: Delete) {
|
||||||
|
def savePath = propertyOrNull("screenshotsSavePath") ?: "build/testscreenshots/"
|
||||||
|
|
||||||
|
delete files("$savePath")
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
connectedDebugAndroidTest.finalizedBy pullScreenshots
|
||||||
|
pullScreenshots.finalizedBy removeScreenshotsFromDevice
|
||||||
|
clean.dependsOn(removeLocalScreenshots)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.fnives.test.showcase.android.testutil.activity
|
||||||
|
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import org.junit.rules.TestWatcher
|
||||||
|
import org.junit.runner.Description
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Rule which closes the given [scenario] safely when the Test is finished.
|
||||||
|
*/
|
||||||
|
class SafeCloseActivityRule(val scenario: () -> ActivityScenario<*>) : TestWatcher() {
|
||||||
|
|
||||||
|
override fun finished(description: Description) {
|
||||||
|
super.finished(description)
|
||||||
|
scenario().safeClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
package org.fnives.test.showcase.android.testutil.screenshot
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.test.runner.screenshot.ScreenCapture
|
||||||
|
import androidx.test.runner.screenshot.ScreenCaptureProcessor
|
||||||
|
import androidx.test.runner.screenshot.Screenshot
|
||||||
|
import org.junit.rules.TestWatcher
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class ScreenshotRule(
|
||||||
|
private val prefix: String = "",
|
||||||
|
private val takeBefore: Boolean = false,
|
||||||
|
private val takeOnSuccess: Boolean = false,
|
||||||
|
private val takeOnFailure: Boolean = true,
|
||||||
|
private val timestampSuffix: Boolean = true,
|
||||||
|
private val processor: ScreenCaptureProcessor = basicScreenCaptureProcessor(),
|
||||||
|
) : TestWatcher() {
|
||||||
|
|
||||||
|
override fun starting(description: Description) {
|
||||||
|
super.starting(description)
|
||||||
|
if (takeBefore) {
|
||||||
|
takeScreenshot(baseName = description.beforeTestScreenshotName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun failed(e: Throwable?, description: Description) {
|
||||||
|
super.failed(e, description)
|
||||||
|
if (takeOnFailure) {
|
||||||
|
takeScreenshot(baseName = description.failTestScreenshotName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun succeeded(description: Description) {
|
||||||
|
super.succeeded(description)
|
||||||
|
if (takeOnSuccess) {
|
||||||
|
takeScreenshot(baseName = description.successTestScreenshotName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun takeScreenshot(prefix: String = this.prefix, baseName: String) {
|
||||||
|
val fileName = if (timestampSuffix) {
|
||||||
|
"$prefix-$baseName-${System.currentTimeMillis()}"
|
||||||
|
} else {
|
||||||
|
"$prefix-$baseName"
|
||||||
|
}
|
||||||
|
takeScreenshot(filename = fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("PrintStackTrace")
|
||||||
|
private fun takeScreenshot(filename: String) {
|
||||||
|
val capture: ScreenCapture = Screenshot.capture()
|
||||||
|
capture.name = filename
|
||||||
|
capture.format = Bitmap.CompressFormat.JPEG
|
||||||
|
try {
|
||||||
|
capture.process(setOf(processor))
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val Description.testScreenshotName get() = "${testClass.simpleName}-$methodName"
|
||||||
|
val Description.beforeTestScreenshotName get() = "$testScreenshotName-BEFORE"
|
||||||
|
val Description.successTestScreenshotName get() = "$testScreenshotName-SUCCESS"
|
||||||
|
val Description.failTestScreenshotName get() = "$testScreenshotName-FAIL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@file:Suppress("PackageDirectoryMismatch")
|
||||||
|
|
||||||
|
package androidx.test.runner.screenshot
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun basicScreenCaptureProcessor(file: File) = BasicScreenCaptureProcessor(file)
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.fnives.test.showcase.android.testutil.screenshot
|
||||||
|
|
||||||
|
import android.os.Environment
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.runner.screenshot.basicScreenCaptureProcessor
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun basicScreenCaptureProcessor(subDir: String = "test-screenshots") =
|
||||||
|
basicScreenCaptureProcessor(File(getTestPicturesDir(), subDir))
|
||||||
|
|
||||||
|
fun getTestPicturesDir() =
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue