Merge pull request #101 from fknives/issue#100-add-screenshots-on-test-fail

Issue#100 Create TestRule Saving Screenshots on UI Test failure
This commit is contained in:
Gergely Hegedis 2022-07-13 16:29:54 +03:00 committed by GitHub
commit 60e96f2beb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 222 additions and 7 deletions

View file

@ -124,3 +124,10 @@ jobs:
name: Emulator-Test-Results-${{ matrix.api-level }}
path: ./**/build/reports/androidTests/**/*.html
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

View file

@ -116,3 +116,4 @@ dependencies {
androidTestImplementation testFixtures(project(':core'))
}
apply from: '../gradlescripts/pull-screenshots.gradle'

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
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.compose.screen.AppNavigation
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
@ -21,8 +22,7 @@ import org.koin.test.KoinTest
@RunWith(AndroidJUnit4::class)
class AuthComposeInstrumentedTest : KoinTest {
@get:Rule
val composeTestRule = createComposeRule()
private val composeTestRule = createComposeRule()
private val stateRestorationTester = StateRestorationTester(composeTestRule)
private val mockServerScenarioSetupTestRule = MockServerScenarioSetupResetingTestRule()
@ -35,6 +35,8 @@ class AuthComposeInstrumentedTest : KoinTest {
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(dispatcherTestRule)
.around(composeTestRule)
.around(ScreenshotRule("test-showcase-compose"))
@Before
fun setup() {

View file

@ -3,7 +3,9 @@ package org.fnives.test.showcase.ui.home
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
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.screenshot.ScreenshotRule
import org.fnives.test.showcase.android.testutil.synchronization.loopMainThreadFor
import org.fnives.test.showcase.model.content.FavouriteContent
import org.fnives.test.showcase.network.mockserver.ContentData
@ -38,6 +40,8 @@ class MainActivityInstrumentedTest : KoinTest {
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(AsyncDiffUtilInstantTestRule())
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule("test-showcase"))
@Before
fun setup() {
@ -48,7 +52,6 @@ class MainActivityInstrumentedTest : KoinTest {
@After
fun tearDown() {
activityScenario.safeClose()
Intents.release()
}

View file

@ -4,7 +4,8 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
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.testutils.MockServerScenarioSetupResetingTestRule
import org.fnives.test.showcase.testutils.idling.MainDispatcherTestRule
@ -32,6 +33,8 @@ class AuthActivityInstrumentedTest : KoinTest {
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule("test-showcase"))
@Before
fun setup() {
@ -41,7 +44,6 @@ class AuthActivityInstrumentedTest : KoinTest {
@After
fun tearDown() {
activityScenario.safeClose()
Intents.release()
}

View file

@ -4,7 +4,8 @@ import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
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.idling.MainDispatcherTestRule
import org.fnives.test.showcase.testutils.statesetup.SetupAuthenticationState.setupLogin
@ -32,6 +33,8 @@ class SplashActivityInstrumentedTest : KoinTest {
@JvmField
val ruleOrder: RuleChain = RuleChain.outerRule(mockServerScenarioSetupTestRule)
.around(mainDispatcherTestRule)
.around(SafeCloseActivityRule { activityScenario })
.around(ScreenshotRule("test-showcase"))
@Before
fun setup() {
@ -41,7 +44,6 @@ class SplashActivityInstrumentedTest : KoinTest {
@After
fun tearDown() {
activityScenario.safeClose()
Intents.release()
}

View file

@ -0,0 +1,88 @@
// 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, returning static: $System.env.HOME/Library/Android/sdk/platform-tools/adb")
return "$System.env.HOME/Library/Android/sdk/platform-tools/adb"
}
}
def packageName = propertyOrNull("screenshotsPackageName") ?: "$android.defaultConfig.applicationId"
def screenshotDirectory = propertyOrNull("screenshotsDirectory") ?: "test-screenshots"
def savePath = propertyOrNull("screenshotsSavePath") ?: "build/testscreenshots/"
def adb = propertyOrNull("adbPath") ?: findAdbFromLocal()
def internalFullPath = "/sdcard/Android/data/$packageName/files/Pictures/$screenshotDirectory/"
def deprecatedFullPath = "/sdcard/Pictures/$packageName/$screenshotDirectory/"
task pullScreenshotsInternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Pull screenshots From internal Storage'
ignoreExitValue(true)
commandLine "$adb", 'pull', "$internalFullPath", "$savePath/"
}
task pullScreenshotsDeprecated(type: Exec) {
group = 'Test-Screenshots'
description = 'Pull screenshots From deprecated External Storage'
ignoreExitValue(true)
commandLine "$adb", 'pull', "$deprecatedFullPath", "$savePath/"
}
task pullScreenshots(dependsOn: [pullScreenshotsInternal, pullScreenshotsDeprecated]) {
group = 'Test-Screenshots'
description = 'Pull screenshots From Device'
}
task removeScreenshotsFromDeviceInternal(type: Exec) {
group = 'Test-Screenshots'
description = 'Remove screenshots From internal Storage'
ignoreExitValue(true)
commandLine "$adb", 'shell', 'rm', '-r', "$internalFullPath"
}
task removeScreenshotsFromDeviceDeprecated(type: Exec) {
group = 'Test-Screenshots'
description = 'Remove screenshots From deprecated External Storage'
ignoreExitValue(true)
commandLine "$adb", 'shell', 'rm', '-r', "$deprecatedFullPath"
}
task removeScreenshotsFromDevice(dependsOn: [removeScreenshotsFromDeviceInternal, removeScreenshotsFromDeviceDeprecated]) {
group = 'Test-Screenshots'
description = 'Remove screenshots From Device'
}
task removeLocalScreenshots(type: Delete) {
group = 'Test-Screenshots'
description = 'Remove screenshots From Local Machine'
delete files("$savePath")
}
afterEvaluate {
connectedDebugAndroidTest.finalizedBy pullScreenshots
pullScreenshots.finalizedBy removeScreenshotsFromDevice
clean.dependsOn(removeLocalScreenshots)
}

View file

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

View file

@ -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"
}
}

View file

@ -0,0 +1,7 @@
@file:Suppress("PackageDirectoryMismatch")
package androidx.test.runner.screenshot
import java.io.File
fun basicScreenCaptureProcessor(file: File) = BasicScreenCaptureProcessor(file)

View file

@ -0,0 +1,19 @@
package org.fnives.test.showcase.android.testutil.screenshot
import android.os.Build
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))
@Suppress("DEPRECATION")
fun getTestPicturesDir() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
InstrumentationRegistry.getInstrumentation().targetContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
} else {
val packageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), packageName)
}