Library that helps you write less code when testing interface in your iOS apps.
Get help using and installing this product on our Slack, channel #help-spectools
After you add SpecTools framework a set of options will become available for most of the UI elements through a spec property. Available for iOS and tvOS
These are:
- action
- simulate tap on a button with specific action event
- simulate gesture recognizer taps
- check
- if a view is truly visible on screen
- view controllers and navigation stacks
- table views have the right content in their cells
- find
- locate any element or elements on the screen based on their content or type
- any text on any UI element
- prepare
- prepare your view controller like it would get setup in a real environment
- change size of the view for any device screen during runtime to check all is still visible
- assign your test view controllers to a mock navigation controller in order to track
pushViewController
orpopViewController
methods
There is a space for a lot more additional functionality so please feel free to raise issues with feature requests.
An example implementation is shown below:
import Foundation
import UIKit
import Quick
import Nimble
import SpecTools
@testable import SpecToolsExample
class ViewControllerSpec: QuickSpec {
override func spec() {
let subject = ViewController()
describe("basic view controller") {
beforeEach {
// Simulate view controller being presented to the screen
subject.spec.prepare.simulatePresentViewController()
// Reset the view to specific size
subject.spec.prepare.set(viewSize: .iPhone6Plus)
}
it("has a visible label1") {
// Get your first label
let element = subject.view.spec.find.first(labelWithText: "My first label")
// Check if the label is truly visible and print out the entire view structure that is being checked
expect(element?.spec.check.isVisible(visualize: .text)).to(beTrue())
}
it("has a visible scrollView") {
// Get a scroll view
let element = subject.view.spec.find.firstScrollView()
// Check visibility
expect(element?.spec.check.isVisible()).to(beTrue())
}
it("has a visible label2", closure: {
// Get a label that contains "second label" and print how we get to it in the console including any text on visible elements
let element = subject.view.spec.find.first(labelWithText: "My second label", exactMatch: false, visualize: .text)
// Check if the label is visible on subjects view and print all frames we encounter on the way
expect(element?.spec.check.isVisible(on: subject.view, visualize: .frames)).to(beTrue())
})
describe("when we tap on button1") {
beforeEach {
// Simulate button tap
button1.spec.action.tap()
}
it("should have pushed new view controller") {
// Check we have new view controller in the navigation stack
expect(subject.navigationController?.spec.check.contains(viewControllerClass: TableViewController.self)).to(beTrue())
}
}
}
}
}
To run the example project:
- Clone the repo, and run
pod install
from the Example directory. - Cmd+U to run example tests
Jazzy based documentation is available here .
Online documentation should always reflect the latest code available on master
branch.
This library can be run completely independently. It does not need quick and nimble although we highly recommend your give these libraries a go!
SpecTools is available through CocoaPods. To install it, simply add the following line to your test target in a Podfile:
target 'SpecToolsExampleTests' do
pod "SpecTools"
end
SpecTools is also available through Carthage. To install it, simply add the following line to your Cartfile and than import the framework into your test target.
github "manGoweb/SpecTools"
Some methods have a debugging mechanisms available. The most common one is visualize: VisualizationType
parameter which allows you to print out debug data (usually a tree of elements SpecTools have recursed through) into the console.
For the visualize options available please refer to the documentation: VisualizationType
Following section should contain all of the methods available
Simulate tap on a UIButton
button1.spec.action.tap()
// or
button1.spec.action.tap(event: .touchUpInside)
Simulate tap on a view with UITapGestureRecognizer(s)
view.spec.action.triggerTap()
// or
view.spec.action.triggerTap(taps: 3, touches: 2)
Simulate tap on a UITableView cell
tableView.spec.action.tap(row: 6)
// or
tableView.spec.action.tap(row: 1, section: 5)
Simulate tap on a UICollectionView cell
collectionView.spec.action.tap(item: 3)
// or
collectionView.spec.action.tap(item: 2, section: 1)
Simulate swipe on a view with UISwipeGestureRecognizer(s)
view.spec.action.triggerSwipe(direction: .up)
Simulate swipe on a view with UILongPressGestureRecognizer(s)
button.spec.action.triggerLongPress()
// or
button.spec.action.triggerLongPress(minimumPressDuration: 1.0, taps: 3, touches: 2)
Execute action on any UIGestureRecognizer
recognizer.spec.action.execute()
Get array of targets from any UIGestureRecognizer
- Return
[(target: AnyObject, action: Selector)]
recognizer.spec.action.getTargetInfo()
Simulate scrolling on any UIScrollView (or table/collection view) while calling all available delegate methods in the right order along the way
- decelerate sets if the scroll view should simulate decelerating after dragging
scrollView.spec.action.scroll(to: CGPoint(x: 500, y: 0), decelerate: true)
Simulate scrolling on any UIScrollView to a specific horizontal page
scrollView.spec.action.scroll(horizontalPageIndex: 2, decelerate: false)
Simulate scrolling on any UIScrollView to a specific vertical page
scrollView.spec.action.scroll(verticalPageIndex: 5, decelerate: true)
Is view truly visible on the screen? Checks if the element (and all parent views) have superview, have alpha, are not hidden and have a valid on-screen frame.
view.spec.check.isVisible() // Example 1
// or
view.spec.check.isVisible(on: viewController.view, visualize: .all) // Example 2
Example 1) Ignores the last view having no superview, expects it to be view controllers view for example
Example 2) Checks visibility against another view, also an entire recursed view structure can be printed to the console
Check if all UITableViewCells available through the UITableView data source evaluate correctly
// Create an enumerate closure
let doesFitClosure: (UITableViewCell)->Bool = { (cell) -> Bool in
guard let cell = cell as? CustomTableViewCell else {
return false
}
if cell.customLabel.text?.characters.count == 0 {
return false
}
return true
}
// Test if all cells generated by the data source are ok using your closure
let ok = subject.tableView.spec.check.allCells(fit: doesFitClosure)
You can also ask for an array of IndexPaths that don't fit the criteria
let indexPaths: [IndexPath] = subject.tableView.spec.check.allCells(thatDontFit: doesFitClosure)
Check if all UICollectionViewCell available through the UICollectionView data source evaluate correctly
// Create an enumerate closure
let doesFitClosure: (UICollectionViewCell)->Bool = { (cell) -> Bool in
guard let cell = cell as? CustomCollectionViewCell else {
return false
}
if cell.customLabel.text?.characters.count == 0 {
return false
}
return true
}
// Test if all cells generated by the data source are ok using your closure
let ok = subject.collectionView.spec.check.allCells(fit: doesFitClosure)
You can also ask for an array of IndexPaths that don't fit the criteria
let indexPaths: [IndexPath] = subject.collectionView.spec.check.allCells(thatDontFit: doesFitClosure)
Look for a specific view controller in your navigation stack
let ok: Bool = viewController.spec.check.has(siblingInNavigationStack: anotherViewController)
Check if a view controller has a child view controller
let ok: Bool = viewController.spec.check.has(childViewController: childViewController)
Check if a view controller has a specific class type in its navigation stack
let ok: Bool = viewController.spec.check.contains(siblingClassInNavigationStack: AnotherViewController.self)
Check if a view controller has specific class type of a child view controller
let ok: Bool = viewController.spec.check.contains(childViewControllerClass: ChildViewController.self)
Check is navigation view controller contains certain view controller
let ok: Bool = navigationController.spec.check.has(viewController: vc)
Check is navigation view controller contains certain type of a view controller
let ok: Bool = viewController.spec.check.contains(viewControllerClass: MyCustomViewController.self)
Find all gesture recognizers of a certain type on a view (generic method)
let recognizers: [UISwipeGestureRecognizer] = view.spec.find.all(gestureRecognizersOfType: UISwipeGestureRecognizer.self)
Find first label (UILabel) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(labelWithText: "My first label")
// or
let element = view.spec.find.first(labelWithText: "first", exactMatch: false, visualize: .text)
Find first text field (UITextField) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(textFieldWithText: "My first text field")
// or
let element = view.spec.find.first(textFieldWithText: "first", exactMatch: false, visualize: .text)
Find first search bar (UISearchBar) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(searchBarWithText: "My first search bar")
// or
let element = view.spec.find.first(searchBarWithText: "first", exactMatch: false, visualize: .text)
Find first text view (UITextView) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(textViewWithText: "My first text view")
// or
let element = view.spec.find.first(textViewWithText: "first", exactMatch: false, visualize: .text)
Find first table view cell (UITableViewCell) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(tableViewCellWithText: "My first cell")
// or
let element = view.spec.find.first(tableViewCellWithText: "first", exactMatch: false, visualize: .text)
Find first button (UIButton) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(buttonWithText: "My first button")
// or
let element = view.spec.find.first(buttonWithText: "first", exactMatch: false, visualize: .text)
Find first table header or footer view (UITableViewHeaderFooterView) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(tableSectionHeaderFooterViewWithText: "My first table header or footer view")
// or
let element = view.spec.find.first(tableSectionHeaderFooterViewWithText: "first", exactMatch: false, visualize: .text)
Find first UITableView's header or footer (not a section header or footer) on a specific view, which contains or matches required string. View you are searching on can hold multiple table views. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(tableHeaderFooterViewWithText: "My first header view")
// or
let element = view.spec.find.first(tableHeaderFooterViewWithText: "first", exactMatch: false, visualize: .text)
Find first view (any UIView) on a specific parent view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(elementWithText: "My first view with some text on a label deep inside")
// or
let element = view.spec.find.first(elementWithText: "deep", exactMatch: false, visualize: .text)
Find first element (any UIView subclass for Element) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use exactMatch: Bool
flag. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(elementOfType: MyCustomView.self, withText: "My first custom view with some text on a label deep inside")
// or
let element = view.spec.find.first(elementOfType: UILabel.self, withText: "custom", exactMatch: false, visualize: .text)
Find first table view (UITableView) on a specific view. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.firstTableView()
// or
let element = view.spec.find.firstTableView(visualize: .frames)
Find first collection view (UICollectionView) on a specific view. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.firstCollectionView()
// or
let element = view.spec.find.firstCollectionView(visualize: .frames)
Find first scroll view (UIScrollView) on a specific view. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.firstScrollView()
// or
let element = view.spec.find.firstScrollView(visualize: .frames)
Find first table header or footer view (UITableViewHeaderFooterView) on a specific view. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.firstTableHeaderFooterView()
// or
let element = view.spec.find.firstTableHeaderFooterView(visualize: .frames)
Find first element (generic method) on a specific view. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.first(elementOfType: MyCustomView.self)
// or
let element = view.spec.find.first(elementOfType: UIButton.self, visualize: .frames)
Find all elements of a certain type on a specific view. To visualize the path to the element, use visualize: VisualizationType
. Please refer to the Debugging section for more details.
let element = view.spec.find.all(elementsOfType: UITextField.self)
// or
let element = view.spec.find.all(elementsOfType: MyCustomView.self, visualize: .frames)
Get a text from specific view in order of it's importance (first text on a text field, than it looks for placeholder if text is empty). You can specify what text you are looking for by using preferablyMatching: String
. If no text matching the string is found, method will revert to it's original order of priorities. Method returns nil if no direct text property is available.
This method only works for UILabel
, UITextField
, UISearchBar
or UITextView
let element = view.spec.find.anyText()
// or
let element = view.spec.find.anyText(preferablyMatching: "Welcome to my fun app!")
Will touch view of a view controller in order to get loadView and viewDidLoad called, than manually calls viewWillAppear and viewDidAppear with animations disabled
viewController.spec.prepare.simulatePresentViewController()
Set a new, specific size for a view controllers view during runtime
viewController.spec.prepare.set(viewSize: CGSize(width: 375.0, height: 1500))
Set a screensize of a desired device on a view of your view controller, you can specify a custom height. Custom height might be useful when scrollviews are present
viewController.spec.prepare.set(viewSize: .iPhone6Plus)
// or
let customHeight: CGFloat = 5000.0
viewController.spec.prepare.set(viewSize: .iPhone6Plus, height: customHeight)
Give view controller a navigation controller
viewController.spec.prepare.assignNavigationController()
// or
viewController.spec.prepare.assignNavigationController(ofClass: CustomNavigationViewController.self)
Give view controller a mock navigation controller which mainly allows for testing push/pop functionality
viewController.spec.prepare.assignMockNavigationController()
- Ondrej Rafaj, developers@mangoweb.cz
- Jonathan Augele, jona2k5@yahoo.com
- David Harris, davidaharris@outlook.com
- Karol Kozub, karol.kozub@gmail.com
- Mateusz Szklarek, mateusz.szklarek@icloud.com
SpecTools is available under the MIT license. See the LICENSE file for more info.