QA Fest 2019. Алексей Альтер-Песоцкий. Snapshot testing with native mobile frameworks

Q
Snapshot testing
with native mobile frameworks
@alter_al
in/apesotskiy
medium.com/xcnotes
2
Contents
1. iOS Snapshot Testing
2. Android Screenshot Testing
3. Challenges
3
Snapshot testing
4
Tool Area Lang Notes
applitools
XCTest, Espresso,
Appium, Calabash
Any
Polyglot,
$$$
screenshot-tests-for-
android
Espresso Java, Kotlin
Support multiple devices,
lack of reports
shot Espresso Java, Kotlin
Cool reports,
support one device
ios-snapshot-test-case XCTest obj-C, Swift
Really flexible,
poor docs
swift-snapshot-testing XCTest obj-C, Swift
Multiformat,
lack of reports
Tool market
5
6
Introduction
7
How it worked
TestClass
XCTestCase
8
How it works
iOSSnapshotTestCase FBSnapshotVerifyView( )FBSnapshotVerifyLayer( )
TestClass
XCTestCase
9
Snapshot
How it works
UIView
CALayer view.layer.sublayers
view.layer
UIImageView(XCUIScreenshot().image)
ViewController().view.subviews
ViewController().view
10
XCUITest
target "SampleUITests" do
use_frameworks!
pod 'iOSSnapshotTestCase'
end
✓ add an additional pod in Podfile:
set the environment variables in our test scheme:
Precondition
• FB_REFERENCE_IMAGE_DIR
• IMAGE_DIFF_DIR
11
Usage
12
20 48
Usageimport XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
XCUIApplication().launch()
}
func test_Snapshot() {
let score = XCUIApplication().staticTexts["score"]
let board = XCUIApplication().otherElements["board"]
let scoreImage = score.screenshot().image
let fullscreen =
XCUIApplication()screenshot().image.fill(element: board).removingStatusBar
FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen")
FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score")
}
}
XCTest
XCUITest
13
Usageimport XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
XCUIApplication().launch()
}
func test_Snapshot() {
let score = XCUIApplication().staticTexts["score"]
let board = XCUIApplication().otherElements["board"]
let scoreImage = score.screenshot().image
let fullscreen =
XCUIApplication()screenshot().image.fill(element: board).removingStatusBar
FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen")
FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score")
}
} 14
Usageimport XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
XCUIApplication().launch()
}
func test_Snapshot() {
let score = XCUIApplication().staticTexts["score"]
let board = XCUIApplication().otherElements["board"]
let scoreImage = score.screenshot().image
let fullscreen =
XCUIApplication()screenshot().image.fill(element: board).removingStatusBar
FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen")
FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score")
}
} 15
Usageimport XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
XCUIApplication().launch()
}
func test_Snapshot() {
let score = XCUIApplication().staticTexts["score"]
let board = XCUIApplication().otherElements["board"]
let scoreImage = score.screenshot().image
let fullscreen =
XCUIApplication()screenshot().image.fill(element: board).removingStatusBar
FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen")
FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score")
}
} 16
Usageimport XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
XCUIApplication().launch()
}
func test_Snapshot() {
let score = XCUIApplication().staticTexts["score"]
let board = XCUIApplication().otherElements["board"]
let scoreImage = score.screenshot().image
let fullscreen =
XCUIApplication()screenshot().image.fill(element: board).removingStatusBar
FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen")
FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score")
}
} 17
Usageimport XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
XCUIApplication().launch()
}
func test_Snapshot() {
let score = XCUIApplication().staticTexts["score"]
let board = XCUIApplication().otherElements["board"]
let scoreImage = score.screenshot().image
let fullscreen =
XCUIApplication()screenshot().image.fill(element: board).removingStatusBar
FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen")
FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score")
}
} 18
Usage
19
Fullscreen Snapshot
Score Snapshot
Original Screen
Usageimport XCTest
import FBSnapshotTestCase
@testable import Product_Module_Name
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
}
func test_Snapshot() {
let game = NumberTileGameViewController(dimension: 4, threshold: 2048)
game.board?.reset()
game.board?.insertTile(at: (1, 1), value: 2)
game.view.subviews.last!.label.text = "12345678"
FBSnapshotVerifyView(game.view, identifier: "wholeView")
FBSnapshotVerifyView(game.board!, identifier: "boardView")
FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer")
FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer")
}
}
XCTest
20
Usageimport XCTest
import FBSnapshotTestCase
@testable import Product_Module_Name
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
}
func test_Snapshot() {
let game = NumberTileGameViewController(dimension: 4, threshold: 2048)
game.board?.reset()
game.board?.insertTile(at: (1, 1), value: 2)
game.view.subviews.last!.label.text = "12345678"
FBSnapshotVerifyView(game.view, identifier: "wholeView")
FBSnapshotVerifyView(game.board!, identifier: "boardView")
FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer")
FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer")
}
}
XCTest
21
Usageimport XCTest
import FBSnapshotTestCase
@testable import Product_Module_Name
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
}
func test_Snapshot() {
let game = NumberTileGameViewController(dimension: 4, threshold: 2048)
game.board?.reset()
game.board?.insertTile(at: (1, 1), value: 2)
game.view.subviews.last!.label.text = "12345678"
FBSnapshotVerifyView(game.view, identifier: "wholeView")
FBSnapshotVerifyView(game.board!, identifier: "boardView")
FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer")
FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer")
}
}
XCTest
22
Usageimport XCTest
import FBSnapshotTestCase
@testable import Product_Module_Name
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
}
func test_Snapshot() {
let game = NumberTileGameViewController(dimension: 4, threshold: 2048)
game.board?.reset()
game.board?.insertTile(at: (1, 1), value: 2)
game.view.subviews.last!.label.text = "12345678"
FBSnapshotVerifyView(game.view, identifier: "wholeView")
FBSnapshotVerifyView(game.board!, identifier: "boardView")
FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer")
FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer")
}
}
XCTest
23
Usageimport XCTest
import FBSnapshotTestCase
@testable import Product_Module_Name
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
}
func test_Snapshot() {
let game = NumberTileGameViewController(dimension: 4, threshold: 2048)
game.board?.reset()
game.board?.insertTile(at: (1, 1), value: 2)
game.view.subviews.last!.label.text = "12345678"
FBSnapshotVerifyView(game.view, identifier: "wholeView")
FBSnapshotVerifyView(game.board!, identifier: "boardView")
FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer")
FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer")
}
}
XCTest
24
Usage
25
Device & App
Whole View Snapshot
Board View Snapshot
Board Layer Snapshot
Score Layer
Snapshot
Snapshot name
26
Identifier argument
Custom
File name options
.screenScale test_name@3x.png
.device test_name_iPhone.png
.OS test_name_12_2.png
.screenSize test_name_320x728.png
Snapshot name
import XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
fileNameOptions = [
FBSnapshotTestCaseFileNameIncludeOption.OS,
FBSnapshotTestCaseFileNameIncludeOption.screenScale
]
}
func test_Snapshot() {
let os = UIDevice.current.systemVersion
let scale = UIScreen.main.scale
FBSnapshotVerifyView(view, identifier: "(os)_(scale)")
}
} 27
import XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
fileNameOptions = [
FBSnapshotTestCaseFileNameIncludeOption.OS,
FBSnapshotTestCaseFileNameIncludeOption.screenScale
]
}
func test_Snapshot() {
let os = UIDevice.current.systemVersion
let scale = UIScreen.main.scale
FBSnapshotVerifyView(view, identifier: "(os)_(scale)")
}
} 28
Snapshot name
import XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
override func setUp() {
super.setUp()
recordMode = true
fileNameOptions = [
FBSnapshotTestCaseFileNameIncludeOption.OS,
FBSnapshotTestCaseFileNameIncludeOption.screenScale
]
}
func test_Snapshot() {
let os = UIDevice.current.systemVersion
let scale = UIScreen.main.scale
FBSnapshotVerifyView(view, identifier: "(os)_(scale)")
}
} 29
Snapshot name
Tolerance
overall: 0.05
How many pixels may not match
perPixel: 0.05
How each pixel may not match
$ $$ $
ref actual ref actual#ff0000 #ff1a1a
30
pass passfail fail
Tolerance
import XCTest
import FBSnapshotTestCase
class SnapshotUITests: FBSnapshotTestCase {
func test_overallTolerance() {
FBSnapshotVerifyView(view, overallTolerance: 0.01)
}
func test_perPixelTolerance() {
FBSnapshotVerifyView(view, perPixelTolerance: 0.1)
}
}
31
Record
XCode
xcodebuild
fastlane
32
Record
lane :record do
scan(
devices: ['iPhone 8 Plus', 'iPhone 7'],
only_testing: ['snapshotTests/Sample', 'snapshotUITests/Sample'],
xcargs: 'RECORD_MODE=true',
scheme: '<test scheme>'
)
end
Fastfile
$ fastlane recordTerminal
33
Report
ref fail diff
34
35
Introduction
36
How it worked
TestClass
AndroidJUnitRunner
37
How it works
ScreenshotRunner
TestClass
AndroidJUnitRunner
Screenshot
snap( ) snapActivity( )
38
Screenshot
How it works
Activity
View
ActivityTestRule(MainActivity::class.java).activity
39
measure( )
layout( )
draw( )LayoutInflater.from(targetContext)
.inflate(viewId)
activityTestRule.activity.findView(viewId)
View(targetContext)
record( )
Precondition
$ pip install mock
$ pip install Pillow
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Terminal
AndroidManifest.xml
40
Precondition
buildscript {
// ...
dependencies {
// ...
classpath 'com.facebook.testing.screenshot:plugin:0.8.0'
}
}
apply plugin: 'com.facebook.testing.screenshot'
android {
// ...
defaultConfig {
// ...
testInstrumentationRunner "com.my.package.ScreenshotTestRunner"
}
}
class ScreenshotTestRunner: AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle) {
ScreenshotRunner.onCreate(this, arguments)
super.onCreate(arguments)
}
override fun finish(resultCode: Int, results: Bundle) {
ScreenshotRunner.onDestroy()
super.finish(resultCode, results)
}
}
root
build.gradle
app
build.gradle
custom
test runner
41
Usage
42
20 48
43
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
44
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
45
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
46
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
47
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
48
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
49
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
50
Usageclass ScreenshotTest {
@get:Rule
var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false)
@Test
fun test_Snapshot_With_Activity() {
val activity = activityTestRule.launchActivity(null)
val score = activity.findViewById<View>(R.id.score)
Screenshot.snapActivity(activity).setName("whole_activity").record()
Screenshot.snap(score).setName("score").record()
}
@Test
fun test_Snapshot() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val view = MainView(context)
view.game.grid.clearGrid()
view.game.grid.insertTile(Tile(Cell(1, 1), 2))
view.game.score = 12345678
ViewHelpers.setupView(view)
.setExactWidthDp(300).setExactHeightDp(500).layout().draw()
Screenshot.snap(view).record()
}
}
Usage
51
Device & App Activity Snapshot
Score Snapshot
View Snapshot
Usage
buildscript {
// ...
}
screenshots {
multipleDevices true
}
$ ./gradlew recordDebugAndroidTestScreenshotTest
$ ./gradlew verifyDebugAndroidTestScreenshotTest
Terminal
app
build.gradle
52
Flexibility & Tolerance
✓Equal
✓Unique
53
Record
54
android {
// ...
defaultConfig {
if (project.gradle.startParameter.taskNames.toString().contains("record")) {
Map<String, String> map = new HashMap<String, String>()
map.put("package", "my.com.samplesnapshot.screenshots")
setTestInstrumentationRunnerArguments map
}
// ...
}
}
app
build.gradle
Terminal $ ./gradlew recordDebugAndroidTestScreenshotTest
Record
55
Report
56
Report
57
extension UIImage {
var removingStatusBar: UIImage? {
// some stuff
}
func fill(el: XCUIElement) -> UIImage {
// some stuff
}
}
Challenges
iOS
XCTest/XCUITest Confusing grey diff images
XCUITest Excess details on snapshots
XCUITest Animation
Common
Repo becomes fat
Snapshot collecting
Lack of reports
58
Android
What is the difference?
Does multipleDevices really work?
iOS
Android
fastlane
perPixelTolerance: 0.05$ export ANDROID_SERIAL=${udid}
usesDrawViewHierarchyInRect
works only in XCTest
Competitors Killer features Limitations
swift-snapshot-
testing
recordMode tolerance
assertSnaphot
as .recursiveDescription
lack of reports
custom assertions rootViewController
Competitors
shot report
support only one
device
59
Challenges
60
github.com/uber/
ios-snapshot-test-case
github.com/facebook/
screenshot-tests-for-android
github.com/pointfreeco/
swift-snapshot-testing
github.com/karumi/
shot
Resources
https://github.com/alter-al/sample_of_ios_snapshot_testing_2048
https://medium.com/xcnotes/snapshot-testing-in-xcuitest-d18ca9bdeae
https://github.com/alter-al/sample_of_android_screenshot_testing_2048
https://medium.com/xcnotes/snapshot-testing-in-espresso-e159cba817d6
61
Q&A
@alter_al
in/apesotskiy
medium.com/xcnotes
1 of 62

Recommended

Java Virtual Keyboard Using Robot, Toolkit and JToggleButton Classes by
Java Virtual Keyboard Using Robot, Toolkit and JToggleButton ClassesJava Virtual Keyboard Using Robot, Toolkit and JToggleButton Classes
Java Virtual Keyboard Using Robot, Toolkit and JToggleButton ClassesAbdul Rahman Sherzad
5.1K views35 slides
Mach-O par Stéphane Sudre by
Mach-O par Stéphane SudreMach-O par Stéphane Sudre
Mach-O par Stéphane SudreCocoaHeads France
6.5K views81 slides
Android Design Patterns by
Android Design PatternsAndroid Design Patterns
Android Design PatternsGodfrey Nolan
4K views49 slides
Alexey Buzdin "Maslow's Pyramid of Android Testing" by
Alexey Buzdin "Maslow's Pyramid of Android Testing"Alexey Buzdin "Maslow's Pyramid of Android Testing"
Alexey Buzdin "Maslow's Pyramid of Android Testing"IT Event
602 views113 slides
MPD2011 | Александр Додатко "Процесс непрерывной интеграции для iOS проектов" by
MPD2011 | Александр Додатко "Процесс непрерывной интеграции для iOS проектов"MPD2011 | Александр Додатко "Процесс непрерывной интеграции для iOS проектов"
MPD2011 | Александр Додатко "Процесс непрерывной интеграции для iOS проектов"ITGinGer
495 views35 slides
Android TDD by
Android TDDAndroid TDD
Android TDDGodfrey Nolan
2.2K views58 slides

More Related Content

Similar to QA Fest 2019. Алексей Альтер-Песоцкий. Snapshot testing with native mobile frameworks

Visual Component Testing -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp... by
Visual Component Testing  -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp...Visual Component Testing  -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp...
Visual Component Testing -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp...Applitools
1.8K views76 slides
MOPCON 2014 - Best software architecture in app development by
MOPCON 2014 - Best software architecture in app developmentMOPCON 2014 - Best software architecture in app development
MOPCON 2014 - Best software architecture in app developmentanistar sung
7.3K views88 slides
Augmented Reality in JavaScript by
Augmented Reality in JavaScriptAugmented Reality in JavaScript
Augmented Reality in JavaScriptZeno Rocha
1.1K views74 slides
UI Testing Best Practices - An Expected Journey by
UI Testing Best Practices - An Expected JourneyUI Testing Best Practices - An Expected Journey
UI Testing Best Practices - An Expected JourneyOren Farhi
5.1K views45 slides
2011 py con by
2011 py con2011 py con
2011 py conEing Ong
522 views32 slides
Android workshop by
Android workshopAndroid workshop
Android workshopMichael Galpin
1.1K views110 slides

Similar to QA Fest 2019. Алексей Альтер-Песоцкий. Snapshot testing with native mobile frameworks(20)

Visual Component Testing -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp... by Applitools
Visual Component Testing  -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp...Visual Component Testing  -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp...
Visual Component Testing -- w/ Gil Tayar (Applitools) and Gleb Bahmutov (Cyp...
Applitools1.8K views
MOPCON 2014 - Best software architecture in app development by anistar sung
MOPCON 2014 - Best software architecture in app developmentMOPCON 2014 - Best software architecture in app development
MOPCON 2014 - Best software architecture in app development
anistar sung7.3K views
Augmented Reality in JavaScript by Zeno Rocha
Augmented Reality in JavaScriptAugmented Reality in JavaScript
Augmented Reality in JavaScript
Zeno Rocha1.1K views
UI Testing Best Practices - An Expected Journey by Oren Farhi
UI Testing Best Practices - An Expected JourneyUI Testing Best Practices - An Expected Journey
UI Testing Best Practices - An Expected Journey
Oren Farhi5.1K views
2011 py con by Eing Ong
2011 py con2011 py con
2011 py con
Eing Ong522 views
Construire une application JavaFX 8 avec gradle by Thierry Wasylczenko
Construire une application JavaFX 8 avec gradleConstruire une application JavaFX 8 avec gradle
Construire une application JavaFX 8 avec gradle
Thierry Wasylczenko2.2K views
Workshop 23: ReactJS, React & Redux testing by Visual Engineering
Workshop 23: ReactJS, React & Redux testingWorkshop 23: ReactJS, React & Redux testing
Workshop 23: ReactJS, React & Redux testing
Visual Engineering1.8K views
Макс Грибов — Использование SpriteKit в неигровых приложениях by CocoaHeads
Макс Грибов — Использование SpriteKit в неигровых приложенияхМакс Грибов — Использование SpriteKit в неигровых приложениях
Макс Грибов — Использование SpriteKit в неигровых приложениях
CocoaHeads501 views
Useful Tools for Making Video Games - XNA (2008) by Korhan Bircan
Useful Tools for Making Video Games - XNA (2008)Useful Tools for Making Video Games - XNA (2008)
Useful Tools for Making Video Games - XNA (2008)
Korhan Bircan1.1K views
Ui perfomance by Cleveroad
Ui perfomanceUi perfomance
Ui perfomance
Cleveroad272 views
HTML5 - Daha Flash bir web? by Ankara JUG
HTML5 - Daha Flash bir web?HTML5 - Daha Flash bir web?
HTML5 - Daha Flash bir web?
Ankara JUG755 views
jQuery Bay Area Conference 2010 by mennovanslooten
jQuery Bay Area Conference 2010jQuery Bay Area Conference 2010
jQuery Bay Area Conference 2010
mennovanslooten2.8K views
Open Cv 2005 Q4 Tutorial by antiw
Open Cv 2005 Q4 TutorialOpen Cv 2005 Q4 Tutorial
Open Cv 2005 Q4 Tutorial
antiw3.2K views
Exploring CameraX from JetPack by Hassan Abid
Exploring CameraX from JetPackExploring CameraX from JetPack
Exploring CameraX from JetPack
Hassan Abid672 views
Unit testing in iOS featuring OCUnit, GHUnit & OCMock by Robot Media
Unit testing in iOS featuring OCUnit, GHUnit & OCMockUnit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Robot Media4.9K views

More from QAFest

QA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилин by
QA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилинQA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилин
QA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилинQAFest
979 views44 slides
QA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The Future by
QA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The FutureQA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The Future
QA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The FutureQAFest
931 views44 slides
QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe... by
QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe...QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe...
QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe...QAFest
322 views131 slides
QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и... by
QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и...QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и...
QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и...QAFest
336 views92 slides
QA Fest 2019. Никита Галкин. Как зарабатывать больше by
QA Fest 2019. Никита Галкин. Как зарабатывать большеQA Fest 2019. Никита Галкин. Как зарабатывать больше
QA Fest 2019. Никита Галкин. Как зарабатывать большеQAFest
389 views40 slides
QA Fest 2019. Сергей Пирогов. Why everything is spoiled by
QA Fest 2019. Сергей Пирогов. Why everything is spoiledQA Fest 2019. Сергей Пирогов. Why everything is spoiled
QA Fest 2019. Сергей Пирогов. Why everything is spoiledQAFest
342 views33 slides

More from QAFest(20)

QA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилин by QAFest
QA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилинQA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилин
QA Fest 2019. Сергій Короленко. Топ веб вразливостей за 40 хвилин
QAFest979 views
QA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The Future by QAFest
QA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The FutureQA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The Future
QA Fest 2019. Анна Чернышова. Self-healing test automation 2.0. The Future
QAFest931 views
QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe... by QAFest
QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe...QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe...
QA Fest 2019. Doug Sillars. It's just too Slow: Testing Mobile application pe...
QAFest322 views
QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и... by QAFest
QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и...QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и...
QA Fest 2019. Катерина Спринсян. Параллельное покрытие автотестами и другие и...
QAFest336 views
QA Fest 2019. Никита Галкин. Как зарабатывать больше by QAFest
QA Fest 2019. Никита Галкин. Как зарабатывать большеQA Fest 2019. Никита Галкин. Как зарабатывать больше
QA Fest 2019. Никита Галкин. Как зарабатывать больше
QAFest389 views
QA Fest 2019. Сергей Пирогов. Why everything is spoiled by QAFest
QA Fest 2019. Сергей Пирогов. Why everything is spoiledQA Fest 2019. Сергей Пирогов. Why everything is spoiled
QA Fest 2019. Сергей Пирогов. Why everything is spoiled
QAFest342 views
QA Fest 2019. Сергей Новик. Между мотивацией и выгоранием by QAFest
QA Fest 2019. Сергей Новик. Между мотивацией и выгораниемQA Fest 2019. Сергей Новик. Между мотивацией и выгоранием
QA Fest 2019. Сергей Новик. Между мотивацией и выгоранием
QAFest249 views
QA Fest 2019. Владимир Никонов. Код Шредингера или зачем и как мы тестируем н... by QAFest
QA Fest 2019. Владимир Никонов. Код Шредингера или зачем и как мы тестируем н...QA Fest 2019. Владимир Никонов. Код Шредингера или зачем и как мы тестируем н...
QA Fest 2019. Владимир Никонов. Код Шредингера или зачем и как мы тестируем н...
QAFest338 views
QA Fest 2019. Владимир Трандафилов. GUI automation of WEB application with SV... by QAFest
QA Fest 2019. Владимир Трандафилов. GUI automation of WEB application with SV...QA Fest 2019. Владимир Трандафилов. GUI automation of WEB application with SV...
QA Fest 2019. Владимир Трандафилов. GUI automation of WEB application with SV...
QAFest227 views
QA Fest 2019. Иван Крутов. Bulletproof Selenium Cluster by QAFest
QA Fest 2019. Иван Крутов. Bulletproof Selenium ClusterQA Fest 2019. Иван Крутов. Bulletproof Selenium Cluster
QA Fest 2019. Иван Крутов. Bulletproof Selenium Cluster
QAFest282 views
QA Fest 2019. Николай Мижигурский. Миссия /*не*/выполнима: гуманитарий собесе... by QAFest
QA Fest 2019. Николай Мижигурский. Миссия /*не*/выполнима: гуманитарий собесе...QA Fest 2019. Николай Мижигурский. Миссия /*не*/выполнима: гуманитарий собесе...
QA Fest 2019. Николай Мижигурский. Миссия /*не*/выполнима: гуманитарий собесе...
QAFest251 views
QA Fest 2019. Володимир Стиран. Чим раніше – тим вигідніше, але ніколи не піз... by QAFest
QA Fest 2019. Володимир Стиран. Чим раніше – тим вигідніше, але ніколи не піз...QA Fest 2019. Володимир Стиран. Чим раніше – тим вигідніше, але ніколи не піз...
QA Fest 2019. Володимир Стиран. Чим раніше – тим вигідніше, але ніколи не піз...
QAFest301 views
QA Fest 2019. Дмитрий Прокопук. Mocks and network tricks in UI automation by QAFest
QA Fest 2019. Дмитрий Прокопук. Mocks and network tricks in UI automationQA Fest 2019. Дмитрий Прокопук. Mocks and network tricks in UI automation
QA Fest 2019. Дмитрий Прокопук. Mocks and network tricks in UI automation
QAFest225 views
QA Fest 2019. Екатерина Дядечко. Тестирование медицинского софта — вызовы и в... by QAFest
QA Fest 2019. Екатерина Дядечко. Тестирование медицинского софта — вызовы и в...QA Fest 2019. Екатерина Дядечко. Тестирование медицинского софта — вызовы и в...
QA Fest 2019. Екатерина Дядечко. Тестирование медицинского софта — вызовы и в...
QAFest243 views
QA Fest 2019. Катерина Черникова. Tune your P’s: the pop-art of keeping testa... by QAFest
QA Fest 2019. Катерина Черникова. Tune your P’s: the pop-art of keeping testa...QA Fest 2019. Катерина Черникова. Tune your P’s: the pop-art of keeping testa...
QA Fest 2019. Катерина Черникова. Tune your P’s: the pop-art of keeping testa...
QAFest376 views
QA Fest 2019. Алиса Бойко. Какнезапутаться в коммуникативных сетях IT by QAFest
QA Fest 2019. Алиса Бойко. Какнезапутаться в коммуникативных сетях ITQA Fest 2019. Алиса Бойко. Какнезапутаться в коммуникативных сетях IT
QA Fest 2019. Алиса Бойко. Какнезапутаться в коммуникативных сетях IT
QAFest209 views
QA Fest 2019. Святослав Логин. Как найти уязвимости в мобильном приложении by QAFest
QA Fest 2019. Святослав Логин. Как найти уязвимости в мобильном приложенииQA Fest 2019. Святослав Логин. Как найти уязвимости в мобильном приложении
QA Fest 2019. Святослав Логин. Как найти уязвимости в мобильном приложении
QAFest607 views
QA Fest 2019. Катерина Шепелєва та Інна Оснач. Що українцям потрібно знати пр... by QAFest
QA Fest 2019. Катерина Шепелєва та Інна Оснач. Що українцям потрібно знати пр...QA Fest 2019. Катерина Шепелєва та Інна Оснач. Що українцям потрібно знати пр...
QA Fest 2019. Катерина Шепелєва та Інна Оснач. Що українцям потрібно знати пр...
QAFest321 views
QA Fest 2019. Антон Серпутько. Нагрузочное тестирование распределенных асинхр... by QAFest
QA Fest 2019. Антон Серпутько. Нагрузочное тестирование распределенных асинхр...QA Fest 2019. Антон Серпутько. Нагрузочное тестирование распределенных асинхр...
QA Fest 2019. Антон Серпутько. Нагрузочное тестирование распределенных асинхр...
QAFest296 views
QA Fest 2019. Петр Тарасенко. QA Hackathon - The Cookbook 22 by QAFest
QA Fest 2019. Петр Тарасенко. QA Hackathon - The Cookbook 22QA Fest 2019. Петр Тарасенко. QA Hackathon - The Cookbook 22
QA Fest 2019. Петр Тарасенко. QA Hackathon - The Cookbook 22
QAFest164 views

Recently uploaded

Thanksgiving!.pdf by
Thanksgiving!.pdfThanksgiving!.pdf
Thanksgiving!.pdfEnglishCEIPdeSigeiro
637 views17 slides
UNIT NO 13 ORGANISMS AND POPULATION.pptx by
UNIT NO 13 ORGANISMS AND POPULATION.pptxUNIT NO 13 ORGANISMS AND POPULATION.pptx
UNIT NO 13 ORGANISMS AND POPULATION.pptxMadhuri Bhande
59 views33 slides
Introduction to AERO Supply Chain - #BEAERO Trainning program by
Introduction to AERO Supply Chain  - #BEAERO Trainning programIntroduction to AERO Supply Chain  - #BEAERO Trainning program
Introduction to AERO Supply Chain - #BEAERO Trainning programGuennoun Wajih
142 views78 slides
ANGULARJS.pdf by
ANGULARJS.pdfANGULARJS.pdf
ANGULARJS.pdfArthyR3
54 views10 slides
MercerJesse3.0.pdf by
MercerJesse3.0.pdfMercerJesse3.0.pdf
MercerJesse3.0.pdfjessemercerail
233 views6 slides

Recently uploaded(20)

UNIT NO 13 ORGANISMS AND POPULATION.pptx by Madhuri Bhande
UNIT NO 13 ORGANISMS AND POPULATION.pptxUNIT NO 13 ORGANISMS AND POPULATION.pptx
UNIT NO 13 ORGANISMS AND POPULATION.pptx
Madhuri Bhande59 views
Introduction to AERO Supply Chain - #BEAERO Trainning program by Guennoun Wajih
Introduction to AERO Supply Chain  - #BEAERO Trainning programIntroduction to AERO Supply Chain  - #BEAERO Trainning program
Introduction to AERO Supply Chain - #BEAERO Trainning program
Guennoun Wajih142 views
ANGULARJS.pdf by ArthyR3
ANGULARJS.pdfANGULARJS.pdf
ANGULARJS.pdf
ArthyR354 views
Interaction of microorganisms with vascular plants.pptx by MicrobiologyMicro
Interaction of microorganisms with vascular plants.pptxInteraction of microorganisms with vascular plants.pptx
Interaction of microorganisms with vascular plants.pptx
NodeJS and ExpressJS.pdf by ArthyR3
NodeJS and ExpressJS.pdfNodeJS and ExpressJS.pdf
NodeJS and ExpressJS.pdf
ArthyR360 views
Education of marginalized and socially disadvantages segments.pptx by GarimaBhati5
Education of marginalized and socially disadvantages segments.pptxEducation of marginalized and socially disadvantages segments.pptx
Education of marginalized and socially disadvantages segments.pptx
GarimaBhati559 views
Introduction to Physiotherapy and Electrotherapy by Sreeraj S R
Introduction to Physiotherapy and ElectrotherapyIntroduction to Physiotherapy and Electrotherapy
Introduction to Physiotherapy and Electrotherapy
Sreeraj S R82 views
Creative Restart 2023: Christophe Wechsler - From the Inside Out: Cultivating... by Taste
Creative Restart 2023: Christophe Wechsler - From the Inside Out: Cultivating...Creative Restart 2023: Christophe Wechsler - From the Inside Out: Cultivating...
Creative Restart 2023: Christophe Wechsler - From the Inside Out: Cultivating...
Taste39 views
Research Methodology (M. Pharm, IIIrd Sem.)_UNIT_IV_CPCSEA Guidelines for Lab... by RAHUL PAL
Research Methodology (M. Pharm, IIIrd Sem.)_UNIT_IV_CPCSEA Guidelines for Lab...Research Methodology (M. Pharm, IIIrd Sem.)_UNIT_IV_CPCSEA Guidelines for Lab...
Research Methodology (M. Pharm, IIIrd Sem.)_UNIT_IV_CPCSEA Guidelines for Lab...
RAHUL PAL53 views
OOPs - JAVA Quick Reference.pdf by ArthyR3
OOPs - JAVA Quick Reference.pdfOOPs - JAVA Quick Reference.pdf
OOPs - JAVA Quick Reference.pdf
ArthyR380 views
JRN 362 - Lecture Twenty-Three (Epilogue) by Rich Hanley
JRN 362 - Lecture Twenty-Three (Epilogue)JRN 362 - Lecture Twenty-Three (Epilogue)
JRN 362 - Lecture Twenty-Three (Epilogue)
Rich Hanley46 views
Ask The Expert! Nonprofit Website Tools, Tips, and Technology.pdf by TechSoup
 Ask The Expert! Nonprofit Website Tools, Tips, and Technology.pdf Ask The Expert! Nonprofit Website Tools, Tips, and Technology.pdf
Ask The Expert! Nonprofit Website Tools, Tips, and Technology.pdf
TechSoup 68 views

QA Fest 2019. Алексей Альтер-Песоцкий. Snapshot testing with native mobile frameworks

  • 1. Snapshot testing with native mobile frameworks
  • 3. Contents 1. iOS Snapshot Testing 2. Android Screenshot Testing 3. Challenges 3
  • 5. Tool Area Lang Notes applitools XCTest, Espresso, Appium, Calabash Any Polyglot, $$$ screenshot-tests-for- android Espresso Java, Kotlin Support multiple devices, lack of reports shot Espresso Java, Kotlin Cool reports, support one device ios-snapshot-test-case XCTest obj-C, Swift Really flexible, poor docs swift-snapshot-testing XCTest obj-C, Swift Multiformat, lack of reports Tool market 5
  • 6. 6
  • 9. How it works iOSSnapshotTestCase FBSnapshotVerifyView( )FBSnapshotVerifyLayer( ) TestClass XCTestCase 9
  • 10. Snapshot How it works UIView CALayer view.layer.sublayers view.layer UIImageView(XCUIScreenshot().image) ViewController().view.subviews ViewController().view 10 XCUITest
  • 11. target "SampleUITests" do use_frameworks! pod 'iOSSnapshotTestCase' end ✓ add an additional pod in Podfile: set the environment variables in our test scheme: Precondition • FB_REFERENCE_IMAGE_DIR • IMAGE_DIFF_DIR 11
  • 13. Usageimport XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true XCUIApplication().launch() } func test_Snapshot() { let score = XCUIApplication().staticTexts["score"] let board = XCUIApplication().otherElements["board"] let scoreImage = score.screenshot().image let fullscreen = XCUIApplication()screenshot().image.fill(element: board).removingStatusBar FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen") FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score") } } XCTest XCUITest 13
  • 14. Usageimport XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true XCUIApplication().launch() } func test_Snapshot() { let score = XCUIApplication().staticTexts["score"] let board = XCUIApplication().otherElements["board"] let scoreImage = score.screenshot().image let fullscreen = XCUIApplication()screenshot().image.fill(element: board).removingStatusBar FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen") FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score") } } 14
  • 15. Usageimport XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true XCUIApplication().launch() } func test_Snapshot() { let score = XCUIApplication().staticTexts["score"] let board = XCUIApplication().otherElements["board"] let scoreImage = score.screenshot().image let fullscreen = XCUIApplication()screenshot().image.fill(element: board).removingStatusBar FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen") FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score") } } 15
  • 16. Usageimport XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true XCUIApplication().launch() } func test_Snapshot() { let score = XCUIApplication().staticTexts["score"] let board = XCUIApplication().otherElements["board"] let scoreImage = score.screenshot().image let fullscreen = XCUIApplication()screenshot().image.fill(element: board).removingStatusBar FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen") FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score") } } 16
  • 17. Usageimport XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true XCUIApplication().launch() } func test_Snapshot() { let score = XCUIApplication().staticTexts["score"] let board = XCUIApplication().otherElements["board"] let scoreImage = score.screenshot().image let fullscreen = XCUIApplication()screenshot().image.fill(element: board).removingStatusBar FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen") FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score") } } 17
  • 18. Usageimport XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true XCUIApplication().launch() } func test_Snapshot() { let score = XCUIApplication().staticTexts["score"] let board = XCUIApplication().otherElements["board"] let scoreImage = score.screenshot().image let fullscreen = XCUIApplication()screenshot().image.fill(element: board).removingStatusBar FBSnapshotVerifyView(UIImageView(image: fullscreen), identifier: "fullscreen") FBSnapshotVerifyView(UIImageView(image: scoreImage), identifier: "score") } } 18
  • 20. Usageimport XCTest import FBSnapshotTestCase @testable import Product_Module_Name class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true } func test_Snapshot() { let game = NumberTileGameViewController(dimension: 4, threshold: 2048) game.board?.reset() game.board?.insertTile(at: (1, 1), value: 2) game.view.subviews.last!.label.text = "12345678" FBSnapshotVerifyView(game.view, identifier: "wholeView") FBSnapshotVerifyView(game.board!, identifier: "boardView") FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer") FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer") } } XCTest 20
  • 21. Usageimport XCTest import FBSnapshotTestCase @testable import Product_Module_Name class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true } func test_Snapshot() { let game = NumberTileGameViewController(dimension: 4, threshold: 2048) game.board?.reset() game.board?.insertTile(at: (1, 1), value: 2) game.view.subviews.last!.label.text = "12345678" FBSnapshotVerifyView(game.view, identifier: "wholeView") FBSnapshotVerifyView(game.board!, identifier: "boardView") FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer") FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer") } } XCTest 21
  • 22. Usageimport XCTest import FBSnapshotTestCase @testable import Product_Module_Name class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true } func test_Snapshot() { let game = NumberTileGameViewController(dimension: 4, threshold: 2048) game.board?.reset() game.board?.insertTile(at: (1, 1), value: 2) game.view.subviews.last!.label.text = "12345678" FBSnapshotVerifyView(game.view, identifier: "wholeView") FBSnapshotVerifyView(game.board!, identifier: "boardView") FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer") FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer") } } XCTest 22
  • 23. Usageimport XCTest import FBSnapshotTestCase @testable import Product_Module_Name class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true } func test_Snapshot() { let game = NumberTileGameViewController(dimension: 4, threshold: 2048) game.board?.reset() game.board?.insertTile(at: (1, 1), value: 2) game.view.subviews.last!.label.text = "12345678" FBSnapshotVerifyView(game.view, identifier: "wholeView") FBSnapshotVerifyView(game.board!, identifier: "boardView") FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer") FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer") } } XCTest 23
  • 24. Usageimport XCTest import FBSnapshotTestCase @testable import Product_Module_Name class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true } func test_Snapshot() { let game = NumberTileGameViewController(dimension: 4, threshold: 2048) game.board?.reset() game.board?.insertTile(at: (1, 1), value: 2) game.view.subviews.last!.label.text = "12345678" FBSnapshotVerifyView(game.view, identifier: "wholeView") FBSnapshotVerifyView(game.board!, identifier: "boardView") FBSnapshotVerifyLayer(game.board!.layer, identifier: "boardLayer") FBSnapshotVerifyLayer(game.view.layer.sublayers.last!, identifier: "sublayer") } } XCTest 24
  • 25. Usage 25 Device & App Whole View Snapshot Board View Snapshot Board Layer Snapshot Score Layer Snapshot
  • 26. Snapshot name 26 Identifier argument Custom File name options .screenScale test_name@3x.png .device test_name_iPhone.png .OS test_name_12_2.png .screenSize test_name_320x728.png
  • 27. Snapshot name import XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true fileNameOptions = [ FBSnapshotTestCaseFileNameIncludeOption.OS, FBSnapshotTestCaseFileNameIncludeOption.screenScale ] } func test_Snapshot() { let os = UIDevice.current.systemVersion let scale = UIScreen.main.scale FBSnapshotVerifyView(view, identifier: "(os)_(scale)") } } 27
  • 28. import XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true fileNameOptions = [ FBSnapshotTestCaseFileNameIncludeOption.OS, FBSnapshotTestCaseFileNameIncludeOption.screenScale ] } func test_Snapshot() { let os = UIDevice.current.systemVersion let scale = UIScreen.main.scale FBSnapshotVerifyView(view, identifier: "(os)_(scale)") } } 28 Snapshot name
  • 29. import XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { override func setUp() { super.setUp() recordMode = true fileNameOptions = [ FBSnapshotTestCaseFileNameIncludeOption.OS, FBSnapshotTestCaseFileNameIncludeOption.screenScale ] } func test_Snapshot() { let os = UIDevice.current.systemVersion let scale = UIScreen.main.scale FBSnapshotVerifyView(view, identifier: "(os)_(scale)") } } 29 Snapshot name
  • 30. Tolerance overall: 0.05 How many pixels may not match perPixel: 0.05 How each pixel may not match $ $$ $ ref actual ref actual#ff0000 #ff1a1a 30 pass passfail fail
  • 31. Tolerance import XCTest import FBSnapshotTestCase class SnapshotUITests: FBSnapshotTestCase { func test_overallTolerance() { FBSnapshotVerifyView(view, overallTolerance: 0.01) } func test_perPixelTolerance() { FBSnapshotVerifyView(view, perPixelTolerance: 0.1) } } 31
  • 33. Record lane :record do scan( devices: ['iPhone 8 Plus', 'iPhone 7'], only_testing: ['snapshotTests/Sample', 'snapshotUITests/Sample'], xcargs: 'RECORD_MODE=true', scheme: '<test scheme>' ) end Fastfile $ fastlane recordTerminal 33
  • 35. 35
  • 39. Screenshot How it works Activity View ActivityTestRule(MainActivity::class.java).activity 39 measure( ) layout( ) draw( )LayoutInflater.from(targetContext) .inflate(viewId) activityTestRule.activity.findView(viewId) View(targetContext) record( )
  • 40. Precondition $ pip install mock $ pip install Pillow <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> Terminal AndroidManifest.xml 40
  • 41. Precondition buildscript { // ... dependencies { // ... classpath 'com.facebook.testing.screenshot:plugin:0.8.0' } } apply plugin: 'com.facebook.testing.screenshot' android { // ... defaultConfig { // ... testInstrumentationRunner "com.my.package.ScreenshotTestRunner" } } class ScreenshotTestRunner: AndroidJUnitRunner() { override fun onCreate(arguments: Bundle) { ScreenshotRunner.onCreate(this, arguments) super.onCreate(arguments) } override fun finish(resultCode: Int, results: Bundle) { ScreenshotRunner.onDestroy() super.finish(resultCode, results) } } root build.gradle app build.gradle custom test runner 41
  • 43. 43 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 44. 44 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 45. 45 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 46. 46 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 47. 47 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 48. 48 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 49. 49 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 50. 50 Usageclass ScreenshotTest { @get:Rule var activityTestRule = ActivityTestRule(MainActivity::class.java, false, false) @Test fun test_Snapshot_With_Activity() { val activity = activityTestRule.launchActivity(null) val score = activity.findViewById<View>(R.id.score) Screenshot.snapActivity(activity).setName("whole_activity").record() Screenshot.snap(score).setName("score").record() } @Test fun test_Snapshot() { val context = InstrumentationRegistry.getInstrumentation().targetContext val view = MainView(context) view.game.grid.clearGrid() view.game.grid.insertTile(Tile(Cell(1, 1), 2)) view.game.score = 12345678 ViewHelpers.setupView(view) .setExactWidthDp(300).setExactHeightDp(500).layout().draw() Screenshot.snap(view).record() } }
  • 51. Usage 51 Device & App Activity Snapshot Score Snapshot View Snapshot
  • 52. Usage buildscript { // ... } screenshots { multipleDevices true } $ ./gradlew recordDebugAndroidTestScreenshotTest $ ./gradlew verifyDebugAndroidTestScreenshotTest Terminal app build.gradle 52
  • 55. android { // ... defaultConfig { if (project.gradle.startParameter.taskNames.toString().contains("record")) { Map<String, String> map = new HashMap<String, String>() map.put("package", "my.com.samplesnapshot.screenshots") setTestInstrumentationRunnerArguments map } // ... } } app build.gradle Terminal $ ./gradlew recordDebugAndroidTestScreenshotTest Record 55
  • 58. extension UIImage { var removingStatusBar: UIImage? { // some stuff } func fill(el: XCUIElement) -> UIImage { // some stuff } } Challenges iOS XCTest/XCUITest Confusing grey diff images XCUITest Excess details on snapshots XCUITest Animation Common Repo becomes fat Snapshot collecting Lack of reports 58 Android What is the difference? Does multipleDevices really work? iOS Android fastlane perPixelTolerance: 0.05$ export ANDROID_SERIAL=${udid} usesDrawViewHierarchyInRect works only in XCTest
  • 59. Competitors Killer features Limitations swift-snapshot- testing recordMode tolerance assertSnaphot as .recursiveDescription lack of reports custom assertions rootViewController Competitors shot report support only one device 59