SlideShare a Scribd company logo
1 of 102
Download to read offline
Testing
Declarative UIs
I
💜
testing
👩🎓
Fast feedback
Not subjective
Repeatable
Safety
Clarity of usage
Documentation
Fun
Testing has always been an after-
thought
iOS community came a
long way
Blogs, Talks,
Best practices
View models / presenters
Routers
Coordinators
SwiftUI
New way of building UI
Not so new way of building UI
Is it testable?
• Business logic
• Navigation flows
• Visual regressions
Business logic
Admin,
In test group
User,
In test group
struct PageFooter: View {
let user: User
let enableSecretFeature: () -> Void
var body: some View {
if user.flags.contains(.newSecretFeature) {
if user.permissions.contains(.updateOrganizationSettings) {
struct PageFooter: View {
let user: User
let enableSecretFeature: () -> Void
var body: some View {
if user.flags.contains(.newSecretFeature) {
if user.permissions.contains(.updateOrganizationSettings) {
return AnyView(
Button(action: enableSecretFeature) {
Text("Enable a secret feature!”)
}
)
} else {
return AnyView(
Text("Ask admin to enable the secret feature.")
)
}
struct PageFooter: View {
let user: User
let enableSecretFeature: () -> Void
var body: some View {
if user.flags.contains(.newSecretFeature) {
if user.permissions.contains(.updateOrganizationSettings) {
return AnyView(
Button(action: enableSecretFeature) {
Text("Enable a secret feature!”)
}
)
} else {
return AnyView(
Text("Ask admin to enable the secret feature.")
)
}
}
else {
return AnyView(EmptyView())
}
}
}
struct PageFooter: View {
let user: User
let enableSecretFeature: () -> Void
var body: some View {
if user.flags.contains(.newSecretFeature) {
if user.permissions.contains(.updateOrganizationSettings) {
return AnyView(
Button(action: enableSecretFeature) {
Text("Enable a secret feature!”)
}
)
} else {
return AnyView(
Text("Ask admin to enable the secret feature.")
)
}
}
else {
return AnyView(EmptyView())
}
}
}
struct PageFooter: View {
let user: User
let enableSecretFeature: () -> Void
var body: some View {
if user.flags.contains(.newSecretFeature) {
if user.permissions.contains(.updateOrganizationSettings) {
return AnyView(
Button(action: enableSecretFeature) {
Text("Enable a secret feature!”)
}
)
} else {
return AnyView(
Text("Ask admin to enable the secret feature.")
)
}
}
else {
return AnyView(EmptyView())
}
}
}
struct PageFooter: View {
let user: User
let enableSecretFeature: () -> Void
var body: some View {
if user.flags.contains(.newSecretFeature) {
if user.permissions.contains(.updateOrganizationSettings) {
return AnyView(
Button(action: enableSecretFeature) {
Text("Enable a secret feature!”)
}
)
} else {
return AnyView(
Text("Ask admin to enable the secret feature.")
)
}
}
else {
return AnyView(EmptyView())
}
}
}
How to test that we get the right view?
import XCTest
import SwiftUI
func testAdminUserInTestGroupSeesCTA() {
let view = PageFooter(
user: User.testGroupUser_admin,
enableSecretFeature: { }
)
let body = view.body
XCTAssertNotNil(body)
// Assert that we got a button... How?
}
import XCTest
import SwiftUI
func testAdminUserInTestGroupSeesCTA() {
let view = PageFooter(
user: User.testGroupUser_admin,
enableSecretFeature: { }
)
let body = view.body
XCTAssertNotNil(body)
// Assert that we got a button... How?
print(body)
}
Console output:
AnyView(storage: SwiftUI.(unknown context at
$7fff2c5f1e64).AnyViewStorage<SwiftUI.Button<SwiftUI.Text>>)
import XCTest
import SwiftUI
func testAdminUserInTestGroupSeesCTA() {
let view = PageFooter(
user: User.testGroupUser_admin,
enableSecretFeature: { }
)
let body = view.body
XCTAssertNotNil(body)
// Assert that we got a button... How?
print(body)
}
More realistic output:
AnyView(storage: SwiftUI.(unknown context at
$7fff2c5f1e64).AnyViewStorage<SwiftUI.VStack<SwiftUI.TupleView<(SwiftUI.
ModifiedContent<SwiftUI.Text, SwiftUI._AlignmentWritingModifier>,
SwiftUI.ModifiedContent<SwiftUI.Button<SwiftUI.Text>,
SwiftUI._BackgroundModifier<SwiftUI.Color>>)>>>)
import SwiftUI
struct PageFooter: View {
enum Mode {
case showsEnableCTA(Action)
case featureReadOnlyInfo
case empty
}
import SwiftUI
struct PageFooter: View {
enum Mode {
case showsEnableCTA(Action)
case featureReadOnlyInfo
case empty
}
// Explicit input
let mode: Mode
import SwiftUI
struct PageFooter: View {
enum Mode {
case showsEnableCTA(Action)
case featureReadOnlyInfo
case empty
}
// Explicit input
let mode: Mode
var body: some View {
// No data transformations; views only
switch mode {
case .showsEnableCTA(let action):
return AnyView(Button(action: action) { Text(mode.title) })
case .featureReadOnlyInfo:
return AnyView(Text(mode.title))
case .empty:
return AnyView(EmptyView())
}
}
}
import Foundation
// “Glue” to application data like user & services
extension PageFooter.Mode {
init(user: User, service: SecretFeatureService) {
guard user.flags.contains(.newSecretFeature) else {
self = .empty
return
}
if user.permissions.contains(.updateOrganizationSettings) {
self = .showsEnableCTA(service.enable)
} else {
self = .showsTextDescription
}
}
}
func testUserInControlGroupDoesNotSeeSpecialView() {
// given that the user is in the control group
let user = User.controlGroupUser
func testUserInControlGroupDoesNotSeeSpecialView() {
// given that the user is in the control group
let user = User.controlGroupUser
// when we create a view for that user
let view = PageFooter(
user: user,
service: SecretFeatureService(enable: { })
)
func testUserInControlGroupDoesNotSeeSpecialView() {
// given that the user is in the control group
let user = User.controlGroupUser
// when we create a view for that user
let view = PageFooter(
user: user,
service: SecretFeatureService(enable: { })
)
// then the mode of the view will be empty
XCTAssertEqual(view.mode, .empty)
}
func testUserInControlGroupDoesNotSeeSpecialView() {
// given that the user is in the control group
let user = User.controlGroupUser
// when we create a view for that user
let view = PageFooter(
user: user,
service: SecretFeatureService(enable: { })
)
// then the mode of the view will be empty
XCTAssertEqual(view.mode, .empty)
}
✅
func testAdminUserInTestGroupSeesCTA() {
// given that the user is admin and is in the test group
let user = User.testGroupUser_admin
// when we create a view for that user with a specific action
let enableServiceCalled = XCTestExpectation(
description: "enable called”
)
let view = PageFooter(
user: user,
service: SecretFeatureService(
enable: { enableServiceCalled.fulfill() }
)
)
// then the mode of the view will be enable CTA
// and the action will be connected to the SecretFeatureService
if case let .showsEnableCTA(action) = view.mode {
action()
} else {
XCTFail("Wrong mode: (view.mode)")
}
wait(for: [enableServiceCalled], timeout: 0.001)
}
func testAdminUserInTestGroupSeesCTA() {
// given that the user is admin and is in the test group
let user = User.testGroupUser_admin
// when we create a view for that user with a specific action
let enableServiceCalled = XCTestExpectation(
description: "enable called”
)
let view = PageFooter(
user: user,
service: SecretFeatureService(
enable: { enableServiceCalled.fulfill() }
)
)
// then the mode of the view will be enable CTA
// and the action will be connected to the SecretFeatureService
if case let .showsEnableCTA(action) = view.mode {
action()
} else {
XCTFail("Wrong mode: (view.mode)")
}
wait(for: [enableServiceCalled], timeout: 0.001)
}
func testAdminUserInTestGroupSeesCTA() {
// given that the user is admin and is in the test group
let user = User.testGroupUser_admin
// when we create a view for that user with a specific action
let enableServiceCalled = XCTestExpectation(
description: "enable called”
)
let view = PageFooter(
user: user,
service: SecretFeatureService(
enable: { enableServiceCalled.fulfill() }
)
)
// then the mode of the view will be enable CTA
// and the action will be connected to the SecretFeatureService
if case let .showsEnableCTA(action) = view.mode {
action()
} else {
XCTFail("Wrong mode: (view.mode)")
}
wait(for: [enableServiceCalled], timeout: 0.001)
}
func testAdminUserInTestGroupSeesCTA() {
// given that the user is admin and is in the test group
let user = User.testGroupUser_admin
// when we create a view for that user with a specific action
let enableServiceCalled = XCTestExpectation(
description: "enable called”
)
let view = PageFooter(
user: user,
service: SecretFeatureService(
enable: { enableServiceCalled.fulfill() }
)
)
// then the mode of the view will be enable CTA
// and the action will be connected to the SecretFeatureService
if case let .showsEnableCTA(action) = view.mode {
action()
} else {
XCTFail("Wrong mode: (view.mode)")
}
wait(for: [enableServiceCalled], timeout: 0.001)
}
✅
import SwiftUI
struct PageFooter: View {
...
// Static value
let mode: Mode
...
}
import SwiftUI
struct PageFooter: View {
...
// Current mode can be observed through an observable object
@ObservedObject var viewModel: PageFooterViewModel
...
}
import Foundation
import Combine
class PageFooterViewModel: ObservableObject {
@Published var mode: PageFooter.Mode
init(appState: AppState) {
// same as before
mode = PageFooter.Mode(
user: appState.user,
service: appState.secretFeatureService
)
import Foundation
import Combine
class PageFooterViewModel: ObservableObject {
@Published var mode: PageFooter.Mode
init(appState: AppState) {
// same as before
mode = PageFooter.Mode(
user: appState.user,
service: appState.secretFeatureService
)
// `mode` current and future values derived from the app state
sinkSubscription = appState.$user
.combineLatest(appState.$secretFeatureService)
.map { PageFooter.Mode(user: $0, service: $1) }
.sink { self.mode = $0 }
}
}
import Foundation
import Combine
class PageFooterViewModel: ObservableObject {
@Published var mode: PageFooter.Mode
private var sinkSubscription: AnyCancellable? = nil
init(appState: AppState) {
// same as before
mode = PageFooter.Mode(
user: appState.user,
service: appState.secretFeatureService
)
// `mode` current and future values derived from the app state
sinkSubscription = appState.$user
.combineLatest(appState.$secretFeatureService)
.map { PageFooter.Mode(user: $0, service: $1) }
.sink { self.mode = $0 }
}
}
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService(enable: {})
)
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService(enable: {})
)
// when we create a view model for that state
let viewModel = PageFooterViewModel(appState: appState)
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService(enable: {})
)
// when we create a view model for that state
let viewModel = PageFooterViewModel(appState: appState)
// then the mode of the view will be read only
XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo)
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService(enable: {})
)
// when we create a view model for that state
let viewModel = PageFooterViewModel(appState: appState)
// then the mode of the view will be read only
XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo)
// when app state changes
appState.user.flags = []
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService(enable: {})
)
// when we create a view model for that state
let viewModel = PageFooterViewModel(appState: appState)
// then the mode of the view will be read only
XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo)
// when app state changes
appState.user.flags = []
// then the mode changes too
XCTAssertEqual(viewModel.mode, .empty)
}
func testUserInTestGroupWillSeeFeatureInfo() {
// given that the user is in the test group
let user = User.testGroupUser
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService(enable: {})
)
// when we create a view model for that state
let viewModel = PageFooterViewModel(appState: appState)
// then the mode of the view will be read only
XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo)
// when app state changes
appState.user.flags = []
// then the mode changes too
XCTAssertEqual(viewModel.mode, .empty)
}
✅
Views should be dumb.
Try not to hide business logic in UI code.
Navigation is business logic too.
struct PageFooter: View {
var body: some View {
// No data transformations; views only
switch mode {
case .showsEnableCTA(let action):
return AnyView(Button(action: action) { Text(mode.title) })
case .featureReadOnlyInfo:
return AnyView(Text(mode.title))
case .empty:
return AnyView(EmptyView())
}
}
}
struct PageFooter: View {
var body: some View {
// No data transformations; views only
switch mode {
case .showsEnableCTA(let action):
return AnyView(
NavigationLink(
destination: ???,
label: {
Text(mode.title)
}
)
) case .featureReadOnlyInfo:
return AnyView(Text(mode.title))
case .empty:
return AnyView(EmptyView())
}
}
}
Side effect
class SecretFeatureService: ObservableObject {
@Published var showingActivationFlow: Bool = false
func enable() {
showingActivationFlow = true
}
static let `default` = SecretFeatureService()
}
struct MainPage: View {
@ObservedObject var viewModel: PageViewModel
var body: some View {
NavigationView {
VStack {
Settings()
PageFooter(viewModel: viewModel.footer)
NavigationLink(
destination: self.activationFlow,
isActive: $viewModel.showingActivationFlow
) {
EmptyView()
}
}
}
}
}
class PageViewModel: ObservableObject {
let footer: PageFooterViewModel
var showingActivationFlow: Binding<Bool> {
return Binding<Bool>(
get: { self.showingActivationSubject.value },
set: { _ in }
)
}
private var subscription: AnyCancellable? = nil
private let showingActivationSubject: CurrentValueSubject<Bool, Never>
init(appState: AppState) {
footer = PageFooterViewModel(appState: appState)
showingActivationSubject = CurrentValueSubject(
appState.secretFeatureService.showingActivationFlow
)
subscription = appState
.secretFeatureService
.$showingActivationFlow
.sink { [weak self] in
self?.showingActivationSubject.value = $0
}
}
}
func testCTATriggersActivationFlow() {
// given that the user is an admin
let user = User.testGroupUser_admin
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService()
)
// and we have a view model for that state
let viewModel = PageViewModel(appState: appState)
func testCTATriggersActivationFlow() {
// given that the user is an admin
let user = User.testGroupUser_admin
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService()
)
// and we have a view model for that state
let viewModel = PageViewModel(appState: appState)
if case let .showsEnableCTA(action) = viewModel.footer.mode {
// given showingActivationFlow is false
XCTAssertFalse(viewModel.showingActivationFlow.wrappedValue)
// when the CTA ation executes
action()
// then showingActivationFlow is true
XCTAssertTrue(viewModel.showingActivationFlow.wrappedValue)
} else {
XCTFail()
}
}
func testCTATriggersActivationFlow() {
// given that the user is an admin
let user = User.testGroupUser_admin
// and we have an App state with that user
let appState = AppState(
user: user,
secretFeatureService: SecretFeatureService()
)
// and we have a view model for that state
let viewModel = PageViewModel(appState: appState)
if case let .showsEnableCTA(action) = viewModel.footer.mode {
// given showingActivationFlow is false
XCTAssertFalse(viewModel.showingActivationFlow.wrappedValue)
// when the CTA ation executes
action()
// then showingActivationFlow is true
XCTAssertTrue(viewModel.showingActivationFlow.wrappedValue)
} else {
XCTFail()
}
}
✅
Visual regressions
I care how views look
Debug previews
1. Create a view model object
2. Configure the view with the view model
3. Customise the preview configuration (device, accessibility settings, etc)
4. See the view rendered with the provided configuration
A. Anytime the code changes, the preview refreshes
WWDC Session 223: Mastering Xcode Previews
Tests
Fast feedback
Not subjective
Repeatable
Safety
Clarity of usage
Documentation
Fun
Fast feedback
Not subjective
Repeatable
Safety
Clarity of usage
Documentation
Fun
Visual validation is
also automatable
I
💜
Snapshot testing
Snapshot testing
1. Create a view model object
2. Configure the view with the view model
3. Customise the snapshot configuration (device, accessibility settings, etc)
4. Snapshot the view rendered with the provided configuration
A. If snapshotting for the first time → save the snapshot
B. Otherwise assert saved and new snapshots match
Snapshot testing
1. Create a view model object
2. Configure the view with the view model
3. Customise the snapshot configuration (device, accessibility settings, etc)
4. Snapshot the view rendered with the provided configuration
A. If snapshotting for the first time → save the snapshot
B. Otherwise assert saved and new snapshots match
Snapshot testing
1. Create a view model object
2. Configure the view with the view model
3. Customise the snapshot configuration (device, accessibility settings, etc)
4. Snapshot the view rendered with the provided configuration
A. If snapshotting for the first time → save the snapshot
B. Otherwise assert saved and new snapshots match
Debug previews
1. Create a view model object
2. Configure the view with the view model
3. Customise the preview configuration (device, accessibility settings, etc)
4. See the view rendered with the provided configuration
Debug previews
+ testing?
fullList favouritesOnly
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
LandmarkList(
viewModel: .favouritesOnly
)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("favouritesOnly")
NavigationView {
LandmarkList(viewModel: .fullList)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("fullList")
}
}
}
fullList favouritesOnly
fullList favouritesOnly
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
LandmarkList(
viewModel: .favouritesOnly
)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("favouritesOnly")
NavigationView {
LandmarkList(viewModel: .fullList)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("fullList")
}
}
}
PreviewDevice
PreviewLayout
PreviewPlatform
EnvironmentValues
EnvironmentValues
Apply Color Schemes
ColorScheme
ColorSchemeContrast
Suppor Accessibility Text Weights
LegibilityWeight
Handle Layout Direction
LayoutDirection
Control Interaction
PresentationMode
EditMode
ControlActiveState
Use Size Classes
UserInterfaceSizeClass
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
LandmarkList(
viewModel: .favouritesOnly
)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("favouritesOnly")
NavigationView {
LandmarkList(viewModel: .fullList)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("fullList")
}
}
}
fullList favouritesOnly
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
LandmarkList(
viewModel: .favouritesOnly
)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("favouritesOnly")
NavigationView {
LandmarkList(viewModel: .fullList)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("fullList")
}
}
}
fullList favouritesOnly
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
LandmarkList(
viewModel: .favouritesOnly
)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("favouritesOnly")
NavigationView {
LandmarkList(viewModel: .fullList)
}
.previewDevice(
PreviewDevice(rawValue: "iPhone SE")
)
.previewDisplayName("fullList")
}
}
}
fullList favouritesOnly
extension Snapshotting
where Value: SwiftUI.View, Format == UIImage {
...
}
extension Snapshotting
where Value: SwiftUI.View, Format == UIImage {
...
}
func testLandmarksList() {
let view = LandmarksList_Previews.previews
assertSnapshot(matching: view, as: .image)
}
protocol DebugPreviewable {
associatedtype ViewModel
associatedtype Preview: View
static var previewViewModels: [PreviewData<ViewModel>] { get }
static func create(from viewModel: ViewModel) -> Preview
}
protocol DebugPreviewable {
associatedtype ViewModel
associatedtype Preview: View
static var previewViewModels: [PreviewData<ViewModel>] { get }
static func create(from viewModel: ViewModel) -> Preview
}
struct PreviewData<ViewModel> {
let name: String
let viewModel: ViewModel
let configurations: [PreviewConfiguration]
}
protocol DebugPreviewable {
associatedtype ViewModel
associatedtype Preview: View
static var previewViewModels: [PreviewData<ViewModel>] { get }
static func create(from viewModel: ViewModel) -> Preview
}
struct PreviewData<ViewModel> {
let name: String
let viewModel: ViewModel
let configurations: [PreviewConfiguration]
}
struct PreviewConfiguration {
let id: String
let modifyView: (AnyView) -> AnyView
}
extension DebugPreviewable where ViewModel: Hashable {
static var debugPreviews: some View {
ForEach(previewViewModels, id: .self) { previewData in
ForEach(previewData.configurations, id: .self) { config in
config.modifyView(
AnyView(
NavigationView {
create(from: previewData.viewModel)
}
)
)
.previewDisplayName("(previewData.name), (config.id)")
}
}
}
}
extension DebugPreviewable where ViewModel: Hashable {
static var debugPreviews: some View {
ForEach(previewViewModels, id: .self) { previewData in
ForEach(previewData.configurations, id: .self) { config in
config.modifyView(
AnyView(
NavigationView {
create(from: previewData.viewModel)
}
)
)
.previewDisplayName("(previewData.name), (config.id)")
}
}
}
}
extension LandmarksList_Previews: DebugPreviewable {
static var previewViewModels: [PreviewData<LandmarkListViewModel>] {
return [
PreviewData(
name: "favouritesOnly",
viewModel: .favouritesOnly,
configurations: PreviewConfiguration.all
),
PreviewData(
name: "fullList",
viewModel: .fullList,
configurations: [.default]
)
]
}
static func create(from viewModel: LandmarkListViewModel) -> some View {
return LandmarkList(viewModel: viewModel)
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
LandmarksList_Previews.debugPreviews
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
LandmarksList_Previews.debugPreviews
}
}
func testLandmarksList() {
LandmarksList_Previews.assertSnapshots() // #onelinetest
}
✅
FavoritesOnly, BigTextFavoritesOnly, DarkFavoritesOnly FullList
FailureDifferenceReference
Take aways
🥡
1. SwiftUI may be new but declarative UI isn’t. We can leverage past experience to
improve testing.
2. We can make our views more testable by extracting their business logic.
3. We can make sure that we notice regressions by combining debug previews and
snapshot testing.
4. We can try and extract navigation from views but it’s difficult right now.
Thank you!
@nataliya_bg

More Related Content

What's hot

iOS Automation: XCUITest + Gherkin
iOS Automation: XCUITest + GherkiniOS Automation: XCUITest + Gherkin
iOS Automation: XCUITest + GherkinKenneth Poon
 
준비하세요 Angular js 2.0
준비하세요 Angular js 2.0준비하세요 Angular js 2.0
준비하세요 Angular js 2.0Jeado Ko
 
Secret unit testing tools
Secret unit testing toolsSecret unit testing tools
Secret unit testing toolsDror Helper
 
Swift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medicaSwift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medicaWashington Miranda
 
iOS UI Testing in Xcode
iOS UI Testing in XcodeiOS UI Testing in Xcode
iOS UI Testing in XcodeJz Chang
 
Automated Xcode 7 UI Testing
Automated Xcode 7 UI TestingAutomated Xcode 7 UI Testing
Automated Xcode 7 UI TestingJouni Miettunen
 
Lessons Learned: Migrating Tests to Selenium v2
Lessons Learned: Migrating Tests to Selenium v2Lessons Learned: Migrating Tests to Selenium v2
Lessons Learned: Migrating Tests to Selenium v2rogerjhu1
 
JS Conf 2018 AU Node.js applications diagnostics under the hood
JS Conf 2018 AU Node.js applications diagnostics under the hoodJS Conf 2018 AU Node.js applications diagnostics under the hood
JS Conf 2018 AU Node.js applications diagnostics under the hoodNikolay Matvienko
 
03 page navigation and data binding in windows runtime apps
03   page navigation and data binding in windows runtime apps03   page navigation and data binding in windows runtime apps
03 page navigation and data binding in windows runtime appsWindowsPhoneRocks
 
Magento2&amp;java script (2)
Magento2&amp;java script (2)Magento2&amp;java script (2)
Magento2&amp;java script (2)EvgeniyKapelko1
 
ASP.NET MVC Internals
ASP.NET MVC InternalsASP.NET MVC Internals
ASP.NET MVC InternalsVitaly Baum
 
Make XCUITest Great Again
Make XCUITest Great AgainMake XCUITest Great Again
Make XCUITest Great AgainKenneth Poon
 

What's hot (19)

iOS Automation: XCUITest + Gherkin
iOS Automation: XCUITest + GherkiniOS Automation: XCUITest + Gherkin
iOS Automation: XCUITest + Gherkin
 
iOS Talks 6: Unit Testing
iOS Talks 6: Unit TestingiOS Talks 6: Unit Testing
iOS Talks 6: Unit Testing
 
준비하세요 Angular js 2.0
준비하세요 Angular js 2.0준비하세요 Angular js 2.0
준비하세요 Angular js 2.0
 
Secret unit testing tools
Secret unit testing toolsSecret unit testing tools
Secret unit testing tools
 
Swift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medicaSwift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medica
 
javascript examples
javascript examplesjavascript examples
javascript examples
 
iOS UI Testing in Xcode
iOS UI Testing in XcodeiOS UI Testing in Xcode
iOS UI Testing in Xcode
 
Jsunit
JsunitJsunit
Jsunit
 
Testy integracyjne
Testy integracyjneTesty integracyjne
Testy integracyjne
 
Automated Xcode 7 UI Testing
Automated Xcode 7 UI TestingAutomated Xcode 7 UI Testing
Automated Xcode 7 UI Testing
 
Discontinuing Reader Matches
Discontinuing Reader MatchesDiscontinuing Reader Matches
Discontinuing Reader Matches
 
Lessons Learned: Migrating Tests to Selenium v2
Lessons Learned: Migrating Tests to Selenium v2Lessons Learned: Migrating Tests to Selenium v2
Lessons Learned: Migrating Tests to Selenium v2
 
JS Conf 2018 AU Node.js applications diagnostics under the hood
JS Conf 2018 AU Node.js applications diagnostics under the hoodJS Conf 2018 AU Node.js applications diagnostics under the hood
JS Conf 2018 AU Node.js applications diagnostics under the hood
 
03 page navigation and data binding in windows runtime apps
03   page navigation and data binding in windows runtime apps03   page navigation and data binding in windows runtime apps
03 page navigation and data binding in windows runtime apps
 
Magento2&amp;java script (2)
Magento2&amp;java script (2)Magento2&amp;java script (2)
Magento2&amp;java script (2)
 
ASP.NET MVC Internals
ASP.NET MVC InternalsASP.NET MVC Internals
ASP.NET MVC Internals
 
Scrollable Test App
Scrollable Test AppScrollable Test App
Scrollable Test App
 
Make XCUITest Great Again
Make XCUITest Great AgainMake XCUITest Great Again
Make XCUITest Great Again
 
Road to Async Nirvana
Road to Async NirvanaRoad to Async Nirvana
Road to Async Nirvana
 

Similar to French kit2019

Lesson07_Spring_Security_API.pdf
Lesson07_Spring_Security_API.pdfLesson07_Spring_Security_API.pdf
Lesson07_Spring_Security_API.pdfScott Anderson
 
Everything You (N)ever Wanted to Know about Testing View Controllers
Everything You (N)ever Wanted to Know about Testing View ControllersEverything You (N)ever Wanted to Know about Testing View Controllers
Everything You (N)ever Wanted to Know about Testing View ControllersBrian Gesiak
 
#win8aca : How and when metro style apps run
#win8aca : How and when metro style apps run#win8aca : How and when metro style apps run
#win8aca : How and when metro style apps runFrederik De Bruyne
 
Rambler.iOS #6: App delegate - разделяй и властвуй
Rambler.iOS #6: App delegate - разделяй и властвуйRambler.iOS #6: App delegate - разделяй и властвуй
Rambler.iOS #6: App delegate - разделяй и властвуйRAMBLER&Co
 
Formacion en movilidad: Conceptos de desarrollo en iOS (IV)
Formacion en movilidad: Conceptos de desarrollo en iOS (IV) Formacion en movilidad: Conceptos de desarrollo en iOS (IV)
Formacion en movilidad: Conceptos de desarrollo en iOS (IV) Mobivery
 
Improving android experience for both users and developers
Improving android experience for both users and developersImproving android experience for both users and developers
Improving android experience for both users and developersPavel Lahoda
 
Droidcon2013 android experience lahoda
Droidcon2013 android experience lahodaDroidcon2013 android experience lahoda
Droidcon2013 android experience lahodaDroidcon Berlin
 
How to instantiate any view controller for free
How to instantiate any view controller for freeHow to instantiate any view controller for free
How to instantiate any view controller for freeBenotCaron
 
Spring mvc my Faviourite Slide
Spring mvc my Faviourite SlideSpring mvc my Faviourite Slide
Spring mvc my Faviourite SlideDaniel Adenew
 
Working effectively with ViewModels and TDD - UA Mobile 2019
Working effectively with ViewModels and TDD - UA Mobile 2019Working effectively with ViewModels and TDD - UA Mobile 2019
Working effectively with ViewModels and TDD - UA Mobile 2019UA Mobile
 
Swift Delhi: Practical POP
Swift Delhi: Practical POPSwift Delhi: Practical POP
Swift Delhi: Practical POPNatasha Murashev
 
Taming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeTaming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeMacoscope
 
Optimize CollectionView Scrolling
Optimize CollectionView ScrollingOptimize CollectionView Scrolling
Optimize CollectionView ScrollingAndrea Prearo
 
Swift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medicaSwift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medicaWashington Miranda
 
Building Large jQuery Applications
Building Large jQuery ApplicationsBuilding Large jQuery Applications
Building Large jQuery ApplicationsRebecca Murphey
 
Practical Protocol-Oriented-Programming
Practical Protocol-Oriented-ProgrammingPractical Protocol-Oriented-Programming
Practical Protocol-Oriented-ProgrammingNatasha Murashev
 
Practialpop 160510130818
Practialpop 160510130818Practialpop 160510130818
Practialpop 160510130818Shahzain Saeed
 
MCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in Swift
MCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in SwiftMCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in Swift
MCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in SwiftPROIDEA
 

Similar to French kit2019 (20)

Lesson07_Spring_Security_API.pdf
Lesson07_Spring_Security_API.pdfLesson07_Spring_Security_API.pdf
Lesson07_Spring_Security_API.pdf
 
Everything You (N)ever Wanted to Know about Testing View Controllers
Everything You (N)ever Wanted to Know about Testing View ControllersEverything You (N)ever Wanted to Know about Testing View Controllers
Everything You (N)ever Wanted to Know about Testing View Controllers
 
Codemotion appengine
Codemotion appengineCodemotion appengine
Codemotion appengine
 
Eclipse Tricks
Eclipse TricksEclipse Tricks
Eclipse Tricks
 
#win8aca : How and when metro style apps run
#win8aca : How and when metro style apps run#win8aca : How and when metro style apps run
#win8aca : How and when metro style apps run
 
Rambler.iOS #6: App delegate - разделяй и властвуй
Rambler.iOS #6: App delegate - разделяй и властвуйRambler.iOS #6: App delegate - разделяй и властвуй
Rambler.iOS #6: App delegate - разделяй и властвуй
 
Formacion en movilidad: Conceptos de desarrollo en iOS (IV)
Formacion en movilidad: Conceptos de desarrollo en iOS (IV) Formacion en movilidad: Conceptos de desarrollo en iOS (IV)
Formacion en movilidad: Conceptos de desarrollo en iOS (IV)
 
Improving android experience for both users and developers
Improving android experience for both users and developersImproving android experience for both users and developers
Improving android experience for both users and developers
 
Droidcon2013 android experience lahoda
Droidcon2013 android experience lahodaDroidcon2013 android experience lahoda
Droidcon2013 android experience lahoda
 
How to instantiate any view controller for free
How to instantiate any view controller for freeHow to instantiate any view controller for free
How to instantiate any view controller for free
 
Spring mvc my Faviourite Slide
Spring mvc my Faviourite SlideSpring mvc my Faviourite Slide
Spring mvc my Faviourite Slide
 
Working effectively with ViewModels and TDD - UA Mobile 2019
Working effectively with ViewModels and TDD - UA Mobile 2019Working effectively with ViewModels and TDD - UA Mobile 2019
Working effectively with ViewModels and TDD - UA Mobile 2019
 
Swift Delhi: Practical POP
Swift Delhi: Practical POPSwift Delhi: Practical POP
Swift Delhi: Practical POP
 
Taming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeTaming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, Macoscope
 
Optimize CollectionView Scrolling
Optimize CollectionView ScrollingOptimize CollectionView Scrolling
Optimize CollectionView Scrolling
 
Swift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medicaSwift Montevideo Meetup - iPhone, una herramienta medica
Swift Montevideo Meetup - iPhone, una herramienta medica
 
Building Large jQuery Applications
Building Large jQuery ApplicationsBuilding Large jQuery Applications
Building Large jQuery Applications
 
Practical Protocol-Oriented-Programming
Practical Protocol-Oriented-ProgrammingPractical Protocol-Oriented-Programming
Practical Protocol-Oriented-Programming
 
Practialpop 160510130818
Practialpop 160510130818Practialpop 160510130818
Practialpop 160510130818
 
MCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in Swift
MCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in SwiftMCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in Swift
MCE^3 - Natasha Murashev - Practical Protocol-Oriented Programming in Swift
 

Recently uploaded

Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Pooja Nehwal
 
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...Niamh verma
 
9892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x79892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x7Pooja Nehwal
 
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...wyqazy
 
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceCALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceanilsa9823
 
Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝
Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝
Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝soniya singh
 
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceCALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceanilsa9823
 

Recently uploaded (7)

Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
Call US Pooja 9892124323 ✓Call Girls In Mira Road ( Mumbai ) secure service,
 
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
Chandigarh Call Girls Service ❤️🍑 9115573837 👄🫦Independent Escort Service Cha...
 
9892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x79892124323 | Book Call Girls in Juhu and escort services 24x7
9892124323 | Book Call Girls in Juhu and escort services 24x7
 
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
哪里有卖的《俄亥俄大学学历证书+俄亥俄大学文凭证书+俄亥俄大学学位证书》Q微信741003700《俄亥俄大学学位证书复制》办理俄亥俄大学毕业证成绩单|购买...
 
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun serviceCALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
CALL ON ➥8923113531 🔝Call Girls Gomti Nagar Lucknow best Night Fun service
 
Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝
Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝
Model Call Girl in Shalimar Bagh Delhi reach out to us at 🔝8264348440🔝
 
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual serviceCALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
CALL ON ➥8923113531 🔝Call Girls Saharaganj Lucknow best sexual service
 

French kit2019

  • 5. Testing has always been an after- thought
  • 6. iOS community came a long way
  • 8. View models / presenters
  • 11. New way of building UI
  • 12. Not so new way of building UI
  • 13. Is it testable? • Business logic • Navigation flows • Visual regressions
  • 15.
  • 17. struct PageFooter: View { let user: User let enableSecretFeature: () -> Void var body: some View { if user.flags.contains(.newSecretFeature) { if user.permissions.contains(.updateOrganizationSettings) {
  • 18. struct PageFooter: View { let user: User let enableSecretFeature: () -> Void var body: some View { if user.flags.contains(.newSecretFeature) { if user.permissions.contains(.updateOrganizationSettings) { return AnyView( Button(action: enableSecretFeature) { Text("Enable a secret feature!”) } ) } else { return AnyView( Text("Ask admin to enable the secret feature.") ) }
  • 19. struct PageFooter: View { let user: User let enableSecretFeature: () -> Void var body: some View { if user.flags.contains(.newSecretFeature) { if user.permissions.contains(.updateOrganizationSettings) { return AnyView( Button(action: enableSecretFeature) { Text("Enable a secret feature!”) } ) } else { return AnyView( Text("Ask admin to enable the secret feature.") ) } } else { return AnyView(EmptyView()) } } }
  • 20. struct PageFooter: View { let user: User let enableSecretFeature: () -> Void var body: some View { if user.flags.contains(.newSecretFeature) { if user.permissions.contains(.updateOrganizationSettings) { return AnyView( Button(action: enableSecretFeature) { Text("Enable a secret feature!”) } ) } else { return AnyView( Text("Ask admin to enable the secret feature.") ) } } else { return AnyView(EmptyView()) } } }
  • 21. struct PageFooter: View { let user: User let enableSecretFeature: () -> Void var body: some View { if user.flags.contains(.newSecretFeature) { if user.permissions.contains(.updateOrganizationSettings) { return AnyView( Button(action: enableSecretFeature) { Text("Enable a secret feature!”) } ) } else { return AnyView( Text("Ask admin to enable the secret feature.") ) } } else { return AnyView(EmptyView()) } } }
  • 22. struct PageFooter: View { let user: User let enableSecretFeature: () -> Void var body: some View { if user.flags.contains(.newSecretFeature) { if user.permissions.contains(.updateOrganizationSettings) { return AnyView( Button(action: enableSecretFeature) { Text("Enable a secret feature!”) } ) } else { return AnyView( Text("Ask admin to enable the secret feature.") ) } } else { return AnyView(EmptyView()) } } } How to test that we get the right view?
  • 23. import XCTest import SwiftUI func testAdminUserInTestGroupSeesCTA() { let view = PageFooter( user: User.testGroupUser_admin, enableSecretFeature: { } ) let body = view.body XCTAssertNotNil(body) // Assert that we got a button... How? }
  • 24. import XCTest import SwiftUI func testAdminUserInTestGroupSeesCTA() { let view = PageFooter( user: User.testGroupUser_admin, enableSecretFeature: { } ) let body = view.body XCTAssertNotNil(body) // Assert that we got a button... How? print(body) } Console output: AnyView(storage: SwiftUI.(unknown context at $7fff2c5f1e64).AnyViewStorage<SwiftUI.Button<SwiftUI.Text>>)
  • 25. import XCTest import SwiftUI func testAdminUserInTestGroupSeesCTA() { let view = PageFooter( user: User.testGroupUser_admin, enableSecretFeature: { } ) let body = view.body XCTAssertNotNil(body) // Assert that we got a button... How? print(body) } More realistic output: AnyView(storage: SwiftUI.(unknown context at $7fff2c5f1e64).AnyViewStorage<SwiftUI.VStack<SwiftUI.TupleView<(SwiftUI. ModifiedContent<SwiftUI.Text, SwiftUI._AlignmentWritingModifier>, SwiftUI.ModifiedContent<SwiftUI.Button<SwiftUI.Text>, SwiftUI._BackgroundModifier<SwiftUI.Color>>)>>>)
  • 26. import SwiftUI struct PageFooter: View { enum Mode { case showsEnableCTA(Action) case featureReadOnlyInfo case empty }
  • 27. import SwiftUI struct PageFooter: View { enum Mode { case showsEnableCTA(Action) case featureReadOnlyInfo case empty } // Explicit input let mode: Mode
  • 28. import SwiftUI struct PageFooter: View { enum Mode { case showsEnableCTA(Action) case featureReadOnlyInfo case empty } // Explicit input let mode: Mode var body: some View { // No data transformations; views only switch mode { case .showsEnableCTA(let action): return AnyView(Button(action: action) { Text(mode.title) }) case .featureReadOnlyInfo: return AnyView(Text(mode.title)) case .empty: return AnyView(EmptyView()) } } }
  • 29. import Foundation // “Glue” to application data like user & services extension PageFooter.Mode { init(user: User, service: SecretFeatureService) { guard user.flags.contains(.newSecretFeature) else { self = .empty return } if user.permissions.contains(.updateOrganizationSettings) { self = .showsEnableCTA(service.enable) } else { self = .showsTextDescription } } }
  • 30. func testUserInControlGroupDoesNotSeeSpecialView() { // given that the user is in the control group let user = User.controlGroupUser
  • 31. func testUserInControlGroupDoesNotSeeSpecialView() { // given that the user is in the control group let user = User.controlGroupUser // when we create a view for that user let view = PageFooter( user: user, service: SecretFeatureService(enable: { }) )
  • 32. func testUserInControlGroupDoesNotSeeSpecialView() { // given that the user is in the control group let user = User.controlGroupUser // when we create a view for that user let view = PageFooter( user: user, service: SecretFeatureService(enable: { }) ) // then the mode of the view will be empty XCTAssertEqual(view.mode, .empty) }
  • 33. func testUserInControlGroupDoesNotSeeSpecialView() { // given that the user is in the control group let user = User.controlGroupUser // when we create a view for that user let view = PageFooter( user: user, service: SecretFeatureService(enable: { }) ) // then the mode of the view will be empty XCTAssertEqual(view.mode, .empty) } ✅
  • 34. func testAdminUserInTestGroupSeesCTA() { // given that the user is admin and is in the test group let user = User.testGroupUser_admin // when we create a view for that user with a specific action let enableServiceCalled = XCTestExpectation( description: "enable called” ) let view = PageFooter( user: user, service: SecretFeatureService( enable: { enableServiceCalled.fulfill() } ) ) // then the mode of the view will be enable CTA // and the action will be connected to the SecretFeatureService if case let .showsEnableCTA(action) = view.mode { action() } else { XCTFail("Wrong mode: (view.mode)") } wait(for: [enableServiceCalled], timeout: 0.001) }
  • 35. func testAdminUserInTestGroupSeesCTA() { // given that the user is admin and is in the test group let user = User.testGroupUser_admin // when we create a view for that user with a specific action let enableServiceCalled = XCTestExpectation( description: "enable called” ) let view = PageFooter( user: user, service: SecretFeatureService( enable: { enableServiceCalled.fulfill() } ) ) // then the mode of the view will be enable CTA // and the action will be connected to the SecretFeatureService if case let .showsEnableCTA(action) = view.mode { action() } else { XCTFail("Wrong mode: (view.mode)") } wait(for: [enableServiceCalled], timeout: 0.001) }
  • 36. func testAdminUserInTestGroupSeesCTA() { // given that the user is admin and is in the test group let user = User.testGroupUser_admin // when we create a view for that user with a specific action let enableServiceCalled = XCTestExpectation( description: "enable called” ) let view = PageFooter( user: user, service: SecretFeatureService( enable: { enableServiceCalled.fulfill() } ) ) // then the mode of the view will be enable CTA // and the action will be connected to the SecretFeatureService if case let .showsEnableCTA(action) = view.mode { action() } else { XCTFail("Wrong mode: (view.mode)") } wait(for: [enableServiceCalled], timeout: 0.001) }
  • 37. func testAdminUserInTestGroupSeesCTA() { // given that the user is admin and is in the test group let user = User.testGroupUser_admin // when we create a view for that user with a specific action let enableServiceCalled = XCTestExpectation( description: "enable called” ) let view = PageFooter( user: user, service: SecretFeatureService( enable: { enableServiceCalled.fulfill() } ) ) // then the mode of the view will be enable CTA // and the action will be connected to the SecretFeatureService if case let .showsEnableCTA(action) = view.mode { action() } else { XCTFail("Wrong mode: (view.mode)") } wait(for: [enableServiceCalled], timeout: 0.001) } ✅
  • 38. import SwiftUI struct PageFooter: View { ... // Static value let mode: Mode ... }
  • 39. import SwiftUI struct PageFooter: View { ... // Current mode can be observed through an observable object @ObservedObject var viewModel: PageFooterViewModel ... }
  • 40. import Foundation import Combine class PageFooterViewModel: ObservableObject { @Published var mode: PageFooter.Mode init(appState: AppState) { // same as before mode = PageFooter.Mode( user: appState.user, service: appState.secretFeatureService )
  • 41. import Foundation import Combine class PageFooterViewModel: ObservableObject { @Published var mode: PageFooter.Mode init(appState: AppState) { // same as before mode = PageFooter.Mode( user: appState.user, service: appState.secretFeatureService ) // `mode` current and future values derived from the app state sinkSubscription = appState.$user .combineLatest(appState.$secretFeatureService) .map { PageFooter.Mode(user: $0, service: $1) } .sink { self.mode = $0 } } }
  • 42. import Foundation import Combine class PageFooterViewModel: ObservableObject { @Published var mode: PageFooter.Mode private var sinkSubscription: AnyCancellable? = nil init(appState: AppState) { // same as before mode = PageFooter.Mode( user: appState.user, service: appState.secretFeatureService ) // `mode` current and future values derived from the app state sinkSubscription = appState.$user .combineLatest(appState.$secretFeatureService) .map { PageFooter.Mode(user: $0, service: $1) } .sink { self.mode = $0 } } }
  • 43. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser
  • 44. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService(enable: {}) )
  • 45. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService(enable: {}) ) // when we create a view model for that state let viewModel = PageFooterViewModel(appState: appState)
  • 46. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService(enable: {}) ) // when we create a view model for that state let viewModel = PageFooterViewModel(appState: appState) // then the mode of the view will be read only XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo)
  • 47. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService(enable: {}) ) // when we create a view model for that state let viewModel = PageFooterViewModel(appState: appState) // then the mode of the view will be read only XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo) // when app state changes appState.user.flags = []
  • 48. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService(enable: {}) ) // when we create a view model for that state let viewModel = PageFooterViewModel(appState: appState) // then the mode of the view will be read only XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo) // when app state changes appState.user.flags = [] // then the mode changes too XCTAssertEqual(viewModel.mode, .empty) }
  • 49. func testUserInTestGroupWillSeeFeatureInfo() { // given that the user is in the test group let user = User.testGroupUser // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService(enable: {}) ) // when we create a view model for that state let viewModel = PageFooterViewModel(appState: appState) // then the mode of the view will be read only XCTAssertEqual(viewModel.mode, .featureReadOnlyInfo) // when app state changes appState.user.flags = [] // then the mode changes too XCTAssertEqual(viewModel.mode, .empty) } ✅
  • 50. Views should be dumb. Try not to hide business logic in UI code. Navigation is business logic too.
  • 51. struct PageFooter: View { var body: some View { // No data transformations; views only switch mode { case .showsEnableCTA(let action): return AnyView(Button(action: action) { Text(mode.title) }) case .featureReadOnlyInfo: return AnyView(Text(mode.title)) case .empty: return AnyView(EmptyView()) } } }
  • 52. struct PageFooter: View { var body: some View { // No data transformations; views only switch mode { case .showsEnableCTA(let action): return AnyView( NavigationLink( destination: ???, label: { Text(mode.title) } ) ) case .featureReadOnlyInfo: return AnyView(Text(mode.title)) case .empty: return AnyView(EmptyView()) } } } Side effect
  • 53. class SecretFeatureService: ObservableObject { @Published var showingActivationFlow: Bool = false func enable() { showingActivationFlow = true } static let `default` = SecretFeatureService() }
  • 54. struct MainPage: View { @ObservedObject var viewModel: PageViewModel var body: some View { NavigationView { VStack { Settings() PageFooter(viewModel: viewModel.footer) NavigationLink( destination: self.activationFlow, isActive: $viewModel.showingActivationFlow ) { EmptyView() } } } } }
  • 55. class PageViewModel: ObservableObject { let footer: PageFooterViewModel var showingActivationFlow: Binding<Bool> { return Binding<Bool>( get: { self.showingActivationSubject.value }, set: { _ in } ) } private var subscription: AnyCancellable? = nil private let showingActivationSubject: CurrentValueSubject<Bool, Never> init(appState: AppState) { footer = PageFooterViewModel(appState: appState) showingActivationSubject = CurrentValueSubject( appState.secretFeatureService.showingActivationFlow ) subscription = appState .secretFeatureService .$showingActivationFlow .sink { [weak self] in self?.showingActivationSubject.value = $0 } } }
  • 56. func testCTATriggersActivationFlow() { // given that the user is an admin let user = User.testGroupUser_admin // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService() ) // and we have a view model for that state let viewModel = PageViewModel(appState: appState)
  • 57. func testCTATriggersActivationFlow() { // given that the user is an admin let user = User.testGroupUser_admin // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService() ) // and we have a view model for that state let viewModel = PageViewModel(appState: appState) if case let .showsEnableCTA(action) = viewModel.footer.mode { // given showingActivationFlow is false XCTAssertFalse(viewModel.showingActivationFlow.wrappedValue) // when the CTA ation executes action() // then showingActivationFlow is true XCTAssertTrue(viewModel.showingActivationFlow.wrappedValue) } else { XCTFail() } }
  • 58. func testCTATriggersActivationFlow() { // given that the user is an admin let user = User.testGroupUser_admin // and we have an App state with that user let appState = AppState( user: user, secretFeatureService: SecretFeatureService() ) // and we have a view model for that state let viewModel = PageViewModel(appState: appState) if case let .showsEnableCTA(action) = viewModel.footer.mode { // given showingActivationFlow is false XCTAssertFalse(viewModel.showingActivationFlow.wrappedValue) // when the CTA ation executes action() // then showingActivationFlow is true XCTAssertTrue(viewModel.showingActivationFlow.wrappedValue) } else { XCTFail() } } ✅
  • 60. I care how views look
  • 61.
  • 62. Debug previews 1. Create a view model object 2. Configure the view with the view model 3. Customise the preview configuration (device, accessibility settings, etc) 4. See the view rendered with the provided configuration A. Anytime the code changes, the preview refreshes
  • 63. WWDC Session 223: Mastering Xcode Previews
  • 64. Tests
  • 65.
  • 68.
  • 71. Snapshot testing 1. Create a view model object 2. Configure the view with the view model 3. Customise the snapshot configuration (device, accessibility settings, etc) 4. Snapshot the view rendered with the provided configuration A. If snapshotting for the first time → save the snapshot B. Otherwise assert saved and new snapshots match
  • 72. Snapshot testing 1. Create a view model object 2. Configure the view with the view model 3. Customise the snapshot configuration (device, accessibility settings, etc) 4. Snapshot the view rendered with the provided configuration A. If snapshotting for the first time → save the snapshot B. Otherwise assert saved and new snapshots match
  • 73. Snapshot testing 1. Create a view model object 2. Configure the view with the view model 3. Customise the snapshot configuration (device, accessibility settings, etc) 4. Snapshot the view rendered with the provided configuration A. If snapshotting for the first time → save the snapshot B. Otherwise assert saved and new snapshots match
  • 74. Debug previews 1. Create a view model object 2. Configure the view with the view model 3. Customise the preview configuration (device, accessibility settings, etc) 4. See the view rendered with the provided configuration
  • 76.
  • 78. struct LandmarksList_Previews: PreviewProvider { static var previews: some View { Group { NavigationView { LandmarkList( viewModel: .favouritesOnly ) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("favouritesOnly") NavigationView { LandmarkList(viewModel: .fullList) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("fullList") } } } fullList favouritesOnly
  • 79. fullList favouritesOnly struct LandmarksList_Previews: PreviewProvider { static var previews: some View { Group { NavigationView { LandmarkList( viewModel: .favouritesOnly ) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("favouritesOnly") NavigationView { LandmarkList(viewModel: .fullList) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("fullList") } } }
  • 81. EnvironmentValues Apply Color Schemes ColorScheme ColorSchemeContrast Suppor Accessibility Text Weights LegibilityWeight Handle Layout Direction LayoutDirection Control Interaction PresentationMode EditMode ControlActiveState Use Size Classes UserInterfaceSizeClass
  • 82. struct LandmarksList_Previews: PreviewProvider { static var previews: some View { Group { NavigationView { LandmarkList( viewModel: .favouritesOnly ) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("favouritesOnly") NavigationView { LandmarkList(viewModel: .fullList) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("fullList") } } } fullList favouritesOnly
  • 83. struct LandmarksList_Previews: PreviewProvider { static var previews: some View { Group { NavigationView { LandmarkList( viewModel: .favouritesOnly ) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("favouritesOnly") NavigationView { LandmarkList(viewModel: .fullList) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("fullList") } } } fullList favouritesOnly
  • 84. struct LandmarksList_Previews: PreviewProvider { static var previews: some View { Group { NavigationView { LandmarkList( viewModel: .favouritesOnly ) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("favouritesOnly") NavigationView { LandmarkList(viewModel: .fullList) } .previewDevice( PreviewDevice(rawValue: "iPhone SE") ) .previewDisplayName("fullList") } } } fullList favouritesOnly
  • 85.
  • 86. extension Snapshotting where Value: SwiftUI.View, Format == UIImage { ... }
  • 87. extension Snapshotting where Value: SwiftUI.View, Format == UIImage { ... } func testLandmarksList() { let view = LandmarksList_Previews.previews assertSnapshot(matching: view, as: .image) }
  • 88. protocol DebugPreviewable { associatedtype ViewModel associatedtype Preview: View static var previewViewModels: [PreviewData<ViewModel>] { get } static func create(from viewModel: ViewModel) -> Preview }
  • 89. protocol DebugPreviewable { associatedtype ViewModel associatedtype Preview: View static var previewViewModels: [PreviewData<ViewModel>] { get } static func create(from viewModel: ViewModel) -> Preview } struct PreviewData<ViewModel> { let name: String let viewModel: ViewModel let configurations: [PreviewConfiguration] }
  • 90. protocol DebugPreviewable { associatedtype ViewModel associatedtype Preview: View static var previewViewModels: [PreviewData<ViewModel>] { get } static func create(from viewModel: ViewModel) -> Preview } struct PreviewData<ViewModel> { let name: String let viewModel: ViewModel let configurations: [PreviewConfiguration] } struct PreviewConfiguration { let id: String let modifyView: (AnyView) -> AnyView }
  • 91. extension DebugPreviewable where ViewModel: Hashable { static var debugPreviews: some View { ForEach(previewViewModels, id: .self) { previewData in ForEach(previewData.configurations, id: .self) { config in config.modifyView( AnyView( NavigationView { create(from: previewData.viewModel) } ) ) .previewDisplayName("(previewData.name), (config.id)") } } } }
  • 92. extension DebugPreviewable where ViewModel: Hashable { static var debugPreviews: some View { ForEach(previewViewModels, id: .self) { previewData in ForEach(previewData.configurations, id: .self) { config in config.modifyView( AnyView( NavigationView { create(from: previewData.viewModel) } ) ) .previewDisplayName("(previewData.name), (config.id)") } } } }
  • 93. extension LandmarksList_Previews: DebugPreviewable { static var previewViewModels: [PreviewData<LandmarkListViewModel>] { return [ PreviewData( name: "favouritesOnly", viewModel: .favouritesOnly, configurations: PreviewConfiguration.all ), PreviewData( name: "fullList", viewModel: .fullList, configurations: [.default] ) ] } static func create(from viewModel: LandmarkListViewModel) -> some View { return LandmarkList(viewModel: viewModel) } }
  • 94. struct LandmarksList_Previews: PreviewProvider { static var previews: some View { LandmarksList_Previews.debugPreviews } }
  • 95. struct LandmarksList_Previews: PreviewProvider { static var previews: some View { LandmarksList_Previews.debugPreviews } } func testLandmarksList() { LandmarksList_Previews.assertSnapshots() // #onelinetest } ✅
  • 99. 🥡 1. SwiftUI may be new but declarative UI isn’t. We can leverage past experience to improve testing. 2. We can make our views more testable by extracting their business logic. 3. We can make sure that we notice regressions by combining debug previews and snapshot testing. 4. We can try and extract navigation from views but it’s difficult right now.
  • 100.
  • 101.