Skip to content

Commit

Permalink
Merge pull request paypal#1 from paypal/V2
Browse files Browse the repository at this point in the history
V2
  • Loading branch information
joleary1987 authored Apr 24, 2017
2 parents 799b33d + c1dd252 commit 76a5d32
Show file tree
Hide file tree
Showing 23 changed files with 1,343 additions and 290 deletions.
255 changes: 255 additions & 0 deletions Docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
# Illuminator TL;DR Manual

1. Copy the `HomeScreen.swift`, `ExampleTestApp.swift`, and `IlluminatorTestCase.swift` files from the example app.
2. Record some element interactions using XCUITest against your app
3. Copy those element interactions into new screen actions as appropriate
4. Remove any anti-patterns as specified in this guide
5. Set breakpoints where it says "(set breakpoint here)"
6. Script your actions together using this basic skeleton:

```swift

func testUsingIlluminatorForTheFirstTime() {
// these 2 lines should go in the setUp() method for your test class,
// or better yet: the IlluminatorTestCase class
let interface = ExampleTestApp(testCase: self)
let initialState = IlluminatorTestProgress<AppTestState>.Passing(AppTestState(didSomething: false))

initialState // The initial state is "passing"
.apply(interface.home.enterText("123")) // to which we apply an action
.apply(interface.home.verifyText("123")) // and another action
.finish(self) // then finalize & handle the result

}

```

7. Run tests and see what happens. Send complaints about this documentation.


#Illuminator Anti-Patterns and How to Fix Them

You don't have to do any of these things, because Illuminator is completely compatible with the XCTest paradigm. (You can even mix & match Illuminator actions with your own XCUITest code, if you need to migrate slowly.) But, you'll get the full benefits of Illuminator if you follow these guidelines.


## Anything `XCT___` Related, Like `XCTAssert` or `XCTFail`

These functions are test cancer -- they prematurely terminate the life of a test before it can tell you anything useful. All you get is a note on how it died. Depressing, right? Right. Avoid them.

### What to Use Instead

Throw exceptions within your actions -- they will automatically be caught and, later, interpreted as a test failure in the `.finish()` method.

For general purpose comparisons, throw `IlluminatorError.VerificationFailed`.

```swift
XCTAssertEqual(app.allElementsBoundByAccessibilityElement.count, 3) // Bad

guard app.allElementsBoundByAccessibilityElement.count == 3 else { //
throw IlluminatorError.VerificationFailed(message: "!= 3") // Good
} // (In practice, wrap it to make a one-liner)
```


For element-centric assertions, use `.assertProperty()` to check any property of an XCUIElement.

```swift
XCTAssert(myElement.exists) // Bad

try myElement.assertProperty(true) { $0.exists } // Good
```


If your concern is only that an element is ready for interactions (it exists and is hittable), use `.ready()`.

```swift
XCTAssert(myElement.hittable) // Bad
myElement.tap() //

try myElement.ready().tap() // Good
```


## Anything Async, Like `expectationForPredicate` or `waitForExpectationsWithTimeout`

Async would be great if it resulted in exceptions instead of test failures, but as of this writing it doesn't. Generally, the need for these functions implies that the app is busy doing something that you need to wait for. Illuminator is, at its core, designed to wait patiently.


### What to Use Instead

Consider this example where we need to wait for the existence of an element to tap it.

```swift
let exists = NSPredicate(format: "exists == 1") //
expectationForPredicate(exists, evaluatedWithObject: myElement) { // Bad
myElement.tap() //
return true //
} //
waitForExpectationsWithTimeout(5, handler: nil) //

try myElement.waitForProperty(5, desired: true) { $0.exists } // Good
myElement.tap() // (you can even chain these calls)
```


## Anything Like `sleep()` or Delaying

Sleeping is a naive way of waiting for some UI change to happen. It's better to simply watch for the change and move on as soon as it happens. There are 2 anti-patterns here, one within a screen and one between screens.


### What to Use Instead

Within the same screen, consider this example where tapping `myButton` causes `someOtherButton` to become visible, after several seconds.

```swift
myButton.tap() //
sleep(3) // Bad
someOtherButton.tap() //

myButton.tap() //
try someOtherButton.whenReady(3).tap() // Good
```

For uses of sleep between screens, see the "Best Practices" section instead.


## XCUIElementQuery Subscripting, Especially When it Might Be Ambiguous

If you want to tap a button called "Delete" in a table cell, and there are multiple cells each with their own "Delete" button, using XCUIElmentQuery to access the button will cause the test to automatically fail. The dreaded `Multiple matches found` error.

This is tragic and unnecessary. Illuminator provides functions to more safely traverse the element tree.


### What to Use Instead

Consider the situation in which multiple matches might appear, but you only ever want the first one.

```swift
// Assume that multiple "Delete" buttons exist
app.buttons["Delete"].tap() // Bad

let matches = app.buttons.subscriptsMatching("Delete") //
guard let myButton = matches[safe: 0] else { //
throw IlluminatorError.VerificationFailed(message: "None") // Good
} //
myButton.tap() //

let matches = app.buttons"Delete" // Experimental unicode operator
```

Or, perhaps you expect one and only one match. Illuminator has an operator for that as well.

```swift
// Assume that multiple "Delete" buttons might exist, but shouldn't
app.buttons["Delete"].tap() // Bad

try app.buttons.hardSubscript("Delete").tap() // Good

try app.buttons"Delete".tap() // Experimental unicode operator
```


# Best Practices

## Make Screens Atomic

If a particular user interaction spans a change of screens, break that into separate actions on separate screens. The most common blunder here is considering a modal to be the same "screen" as the page it obscures.

For example:

```swift
// snip from what might be an "account" screen

public override var isActive: Bool {
return app.navigationBars["Account"].exists
}

func logIn(username: String, password: String) -> IlluminatorActionGeneric<AppTestState> {
return makeAction() {
app.buttons["Log in"].tap() // trigger the login modal
sleep(1) // THIS IS BAD
app.textFields["Username"].typeText(username) // THIS IS A NEW SCREEN
app.textFields["Password"].typeText(password)
app.buttons["Submit credentials"].tap()
}
}
```

### What to Do Instead

Split this action into two separate screens. First, the Account screen:

```swift
public override var isActive: Bool {
return app.navigationBars["Account"].exists
}

func openLoginModal() -> IlluminatorActionGeneric<AppTestState> {
return makeAction() {
try app.buttons["Log in"].ready().tap()
}
}
```

Next, the modal screen:

```swift
public override var isActive: Bool {
return app.buttons["Submit credentials"].exists
}

func logIn(username: String, password: String) -> IlluminatorActionGeneric<AppTestState> {
return makeAction() {
try app.textFields["Username"].ready().typeText(username)
app.textFields["Password"].typeText(password)
app.buttons["Submit credentials"].tap()
}
}
```


## Use Protocol Extensions to Provide Common functions

Consider the case where you have the same tab bar of navigational elements on several screens. Rather than making a base screen that the others inherit from, it's easier (and necessary, just in case you'd run into multiple inheritance problems) to define a protocol extension for screens, that supply the extra features.

```swift
protocol TabBarScreen {
var app: XCUIApplication { get }
var testCaseWrapper: IlluminatorTestcaseWrapper { get }
func makeAction(label l: String, task: () throws -> ()) -> IlluminatorActionGeneric<AppTestState>
}

extension TabBarScreen {
// The tab bar should be able to assert its own existence
var tabBarIsActive: Bool {
get {
return app.tabBars.elementBoundByIndex(0).exists
}
}

func toHome() -> IlluminatorActionGeneric<AppTestState> {
return makeAction(label: #function) {
try self.app.tabBars.buttons["Home"].whenReady(3).tap()
}
}
}
```


Adding tab bar functionality to a screen is now as simple as marking it with the protocol.

```swift
public class HomeScreen: IlluminatorDelayedScreen<AppTestState>, SearchFieldScreen, TabBarScreen {
// consider the tab bar in whether the screen is active
public override var isActive: Bool {
guard tabBarIsActive else { return false }
guard searchScreenIsActive else { return false }
return app.navigationBars["Home"].exists
}
```

# Common Pitfalls

### "I watched my app fail, but the test passed"

Make sure that your `IlluminatorTestProgress` variable calls `.finish()`, otherwise `XCFail()` will never trigger.
8 changes: 8 additions & 0 deletions Example/Illuminator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
5105C42A1E3AF7E700BC2E93 /* IlluminatorTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105C4291E3AF7E700BC2E93 /* IlluminatorTestCase.swift */; };
5105C42C1E3B6AA100BC2E93 /* IlluminatorTestComparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5105C42B1E3B6AA100BC2E93 /* IlluminatorTestComparison.swift */; };
518C87351DF5B71C00104CAD /* ExampleTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C87341DF5B71C00104CAD /* ExampleTestApp.swift */; };
518C87371DF5B7A900104CAD /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518C87361DF5B7A900104CAD /* HomeScreen.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
Expand All @@ -31,6 +33,8 @@

/* Begin PBXFileReference section */
050568E587F4D79A7B282BEC /* Pods-IlluminatorUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IlluminatorUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-IlluminatorUITests/Pods-IlluminatorUITests.release.xcconfig"; sourceTree = "<group>"; };
5105C4291E3AF7E700BC2E93 /* IlluminatorTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IlluminatorTestCase.swift; sourceTree = "<group>"; };
5105C42B1E3B6AA100BC2E93 /* IlluminatorTestComparison.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IlluminatorTestComparison.swift; sourceTree = "<group>"; };
518C87341DF5B71C00104CAD /* ExampleTestApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleTestApp.swift; path = AppDefinition/ExampleTestApp.swift; sourceTree = "<group>"; };
518C87361DF5B7A900104CAD /* HomeScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HomeScreen.swift; path = AppDefinition/Screens/HomeScreen.swift; sourceTree = "<group>"; };
607FACD01AFB9204008FA782 /* Illuminator_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Illuminator_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -156,7 +160,9 @@
children = (
518C87321DF5B6F800104CAD /* AppDefinition */,
881FF4011BD142A50094DFC3 /* IlluminatorUITests.swift */,
5105C4291E3AF7E700BC2E93 /* IlluminatorTestCase.swift */,
881FF4031BD142A50094DFC3 /* Info.plist */,
5105C42B1E3B6AA100BC2E93 /* IlluminatorTestComparison.swift */,
);
path = IlluminatorUITests;
sourceTree = "<group>";
Expand Down Expand Up @@ -384,8 +390,10 @@
buildActionMask = 2147483647;
files = (
518C87351DF5B71C00104CAD /* ExampleTestApp.swift in Sources */,
5105C42C1E3B6AA100BC2E93 /* IlluminatorTestComparison.swift in Sources */,
881FF4021BD142A50094DFC3 /* IlluminatorUITests.swift in Sources */,
518C87371DF5B7A900104CAD /* HomeScreen.swift in Sources */,
5105C42A1E3AF7E700BC2E93 /* IlluminatorTestCase.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
14 changes: 4 additions & 10 deletions Example/IlluminatorUITests/AppDefinition/ExampleTestApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,25 @@ import XCTest
import Illuminator


// Actions (part of screens, which are part of the app) don't contain state.
// The current state of the app is passed to them -- for example:
// - whether a one-time alert has been dismissed might affect the expected
// value or behavior of an action.
// - knowing that a mock network request has been asked to fail
//
// Actions can both read and write the state object, or ignore it completely
//
// In this example, we just use a simple (named) boolean flag.
// For more information on what this is for, see the IlluminatorAction class description
struct AppTestState: CustomStringConvertible {
var didSomething: Bool
var description: String {
get { return "\(didSomething)" }
}
}


// The basic structure; this is a minimum implementation
struct ExampleTestApp: IlluminatorApplication {
let label: String = "ExampleApp"
let testCaseWrapper: IlluminatorTestcaseWrapper

init(testCase t: XCTestCase) {
testCaseWrapper = IlluminatorTestcaseWrapper(testCase: t)
}


// all screens are defined as read-only variables to save boilerplate code in the test definitions
var home: HomeScreen {
get {
return HomeScreen(testCaseWrapper: testCaseWrapper)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,18 @@ class HomeScreen: IlluminatorDelayedScreen<AppTestState> {
func verifyText(expected: String) -> IlluminatorActionGeneric<AppTestState> {
return makeAction() {
let textField = self.app.otherElements.containingType(.Button, identifier:"Button").childrenMatchingType(.TextField).element
XCTAssertEqual(textField.value as? String, expected)
try textField.assertProperty(expected) {
guard let value = $0.value else { return "" }
guard let valString = value as? String else { return "" }
return valString
}
}
}

func doSomething(thing: Bool) -> IlluminatorActionGeneric<AppTestState> {
return makeAction() { (state: AppTestState) in
let newState = AppTestState(didSomething: thing)
return newState
}
}

Expand Down
Loading

0 comments on commit 76a5d32

Please sign in to comment.