Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Опыт внедрения скриншот-тестирования

Опыт внедрения скриншот-тестирования

Презентация моего доклада на конференции DUMP-2023

Олег Осипенко

April 21, 2023
Tweet

Other Decks in Programming

Transcript

  1. О чем этот доклад - сторонние библиотеки - почему Shot

    - проблемы Shot - инструменты для создания 
 собственной библиотеки
  2. О чем этот доклад - сторонние библиотеки - почему Shot

    - проблемы Shot - инструменты для создания 
 собственной библиотеки - архитектура решения
  3. О чем этот доклад - сторонние библиотеки - почему Shot

    - проблемы Shot - инструменты для создания 
 собственной библиотеки - архитектура решения - что можно улучшить
  4. Shot интеграция buildscript { dependencies { classpath 'com.karumi:shot:<LATEST_RELEASE>' } }

    apply plugin: 'shot' android { defaultConfig { testInstrumentationRunner "com.karumi.shot.ShotTestRunner" } }
  5. Shot интеграция class MyActivityTest: ScreenshotTest { @Test fun theActivityIsShownProperly() {

    val mainActivity = startMainActivity() compareScreenshot(mainActivity) } }
  6. Пайплайн тестирования 1. Прогон тестов и создание скриншотов 2. Получение

    скриншотов с девайса 3. Сравнение скриншотов с образцом 4. Генерация тест-репорта
  7. public final class Screenshot { static ScreenCapture capture(); static ScreenCapture

    capture(Activity activity); static ScreenCapture capture(View view); } public final class ScreenCapture { Bitmap getBitmap(); } public interface ScreenCaptureProcessor { abstract String process(ScreenCapture capture); } Google android testing library
  8. Пайплайн тестирования 1. Прогон тестов и создание скриншотов ✅ 2.

    Получение скриншотов с девайса ❓ 3. Сравнение скриншотов с образцом ❓ 4. Генерация тест-репорта ❓
  9. Phash как частный случай LSH Locality-sensitive hashing (LSH) — вероятностный

    метод понижения размерности многомерных данных. Основная идея состоит в таком подборе хеш-функций для некоторых измерений, чтобы похожие объекты с высокой степенью вероятности попадали в одну корзину.
  10. Пайплайн тестирования 1. Прогон тестов и создание скриншотов ✅ 2.

    Получение скриншотов с девайса ❓ 3. Сравнение скриншотов с образцом ✅ 4. Генерация тест-репорта ❓
  11. Пайплайн тестирования 1. Прогон тестов и создание скриншотов ✅ 2.

    Получение скриншотов с девайса ❓ 3. Сравнение скриншотов с образцом ✅ 4. Генерация тест-репорта ✅
  12. Пайплайн тестирования 1. Прогон тестов и создание скриншотов ✅ 2.

    Получение скриншотов с девайса ✅ 3. Сравнение скриншотов с образцом ✅ 4. Генерация тест-репорта ✅
  13. open class BaseScreenshotTest { @get:Rule val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(

    Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ) } fun View.takeScreenshot() { disableFlakyComponentsAndWaitForIdle(this) Screenshot.capture(this).process() } fun Fragment.takeScreenshot() fun Activity.takeScreenshot() fun SemanticsNodeInteraction.takeScreenshot() fun takeScreenshot(bitmap: Bitmap, name: String) BaseScreenshotTest
  14. class FirebaseScreenCaptureProcessor : BasicScreenCaptureProcessor() { init { mDefaultScreenshotPath = File("/sdcard/screenshots")

    } override fun getDefaultFilename(): String = getFileName() companion object { fun getFileName(): String { for (element in Thread.currentThread().stackTrace) { if (isTestMethod) { return «$element.className_$element.methodName" } } } } } FirebaseScreenshotCaptureProcessor
  15. internal class FooterTest: BaseScreenshotTest() { @get:Rule val composeRule = createComposeRule()

    @Test fun testFooter() { composeRule.setContent { Footer( text = "Contact the dispatcher" ) } composeRule.onRoot().takeScreenshot() } } Пример теста
  16. class ScreenshotRecorder { fun recordScreenshots(project: Project) { if (hasRecordedScreenshots()) {

    recordedScreenshots() .filter { it.extension.equals("png", true) } .forEach { recordedFile -> val referenceFile = Path.of("$referenceDirectory/${recordedFile.fileName}") val copy = recordedFile.copyTo(referenceFile, true) } } else { Console.printWarning("Module :${project.name} doesn't have recorded screenshots. Skip") } } } Запись скриншотов
  17. class ScreenshotVerifier{ fun processScreenshot(recorded: Screenshot): ScreenshotComparisonResult { val reference =

    recorded.getReference() return if (reference.notExists()) { nonExistingReference() } else { val recordedImageSize = imageMagick.getImageSize(recorded) val referenceImageSize = imageMagick.getImageSize(reference) when { recordedImageSize != referenceImageSize -> sizeHasChanged() imageMagick.screenshotsAreDifferent() -> screenshotInvalid() else -> screenshotValid() } } } } Screenshot verifier
  18. android { testOptions { managedDevices { devices { add(create<ManagedVirtualDevice>("shotter") {

    device = "Pixel" apiLevel = 28 systemImageSource = "aosp" require64Bit = false }) } } } } ./gradlew app:shotterCheck Gradle managed device
  19. android { defaultConfig { testInstrumentationRunnerArguments += mapOf("useTestStorageService" to "true") }

    } dependencies { androidTestUtil("androidx.test.services:test-services:$servicesVersion" } Сохраняет скриншоты в папку: build/outputs/managed_device_android_test_additional_output TestStorageService
  20. import androidx.test.espresso.screenshot.captureToBitmap import androidx.test.core.graphics.writeToTestStorage open class BaseScreenshotTest { @get:Rule val

    permissionRule: GrantPermissionRule = GrantPermissionRule.grant( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE ) fun captureScreenshot() { onView(withId(android.R.id.content)) .captureToBitmap().writeToTestStorage(«name") } } BaseScreenshotTest
  21. { "viewHierarchy": { "class": "android.widget.FrameLayout", "left": 0, "top": 0, "width":

    1080, "height": 1794, "children": [ { "class": "android.widget.ImageView", "left": 455, "top": 801, "width": 170, "height": 191 } ] }, "version": 1, "axHierarchy": {} } Cоздание скриншотов в Shot
  22. abstract class ScreenshotVerificationTask : DefaultTask() { @get:Incremental @get:InputDirectory abstract val

    referenceDirectory: DirectoryProperty @get:Incremental @get:InputDirectory abstract val recordedDirectory: DirectoryProperty @get:OutputDirectory abstract val reportDirectory: DirectoryProperty @TaskAction fun execute(inputChanges: InputChanges) { } } Инкрементальность
  23. Roborazzi Paparazzi is a great tool to see the actual

    display in the JVM. Paparazzi relies on LayoutLib, Android Studio's layout drawing tool. Robolectric 4.10 adds support for native Android graphics. When native graphics is enabled, interactions with Android graphics classes use real native Android graphics code and are much higher fidelity