Swerkin is an instrumentation testing framework with 5 main objectives:
-
Extend existing open-source iOS instrumentation libraries.
For now, Swerkin extends KIF - Keep it Functional but in future releases could extend XCUITest and/or Earl Grey.
-
Tests built from the frameworks use Gherkin-like syntax (Given/When/Then).
This makes the setup of the test, the action the test is taking, and what the test is verifying easy to read and understand.
The Gherkin-like syntax makes it easier to build the test scenarios as a collaboration between developers and non-developers
-
Provide a catalog of predefined step definitions and a pattern to write new step definitions in Swift.
The ability to create test scenarios should be easy for developers and non-developers but should provide a familiar experience for developers; which includes using intellisense to finds the steps, writing the test scenarios in Swift, and the ability to debug the steps within XCode.
-
Ability to test individual screens without having to navigate through entire application.
This allows for targeted testing and reduces testing times for screens that may appear deep within the application.
-
Ability to test entire flows in an easy to read and succinct manner.
Retaining the ability to test entire flows in addition to targeted testing gives you the best of both worlds. Utilizing the Gherkin-like syntax for the flow tests, allows for better understanding of the test setup and its verification.
Please see the Contributing file on how to contribute to this framework.
Swerkin is available under the MIT license. See the LICENSE file for more info.
There are 3 major components of the Swerkin framework: Base Test Case, Step Definitions, and Screen Objects.
The BaseTestCase is the foundation of every test class created from this framework. It provides several features that can be utilized by the tests within each test class.
-
Test Tags: The ability to add specific tags to test classes and test functions to build dynamic test suites.
-
TestInfo: A dictionary to store test specific data needed for each test.
-
Timeouts: (configurable)
- testTimeout: timeout for entire test before failing (default: 60 seconds)
- waitingTimeout: timeout for waiting for a condition to be true (default: 2 seconds)
- validationTimeout: timeout before failing validations (default: 2 seconds)
-
Preconditions: A dictionary to hold setup data that can be used to determine how a flow and/or end-to-end test is executed.
-
CurrentScreen: A presentable screen on which the test begins.
-
ScreenPresenter: A specific class that provides the ability to register all the screen objects under test and can return a specific screen when needed.
-
Swerkin Objects: including Given / When / Then / And, to provide the Gherkin-like experience and can be extended further if other Gherkin syntax is required.
Swerkin step definitions are divided into three types:
- Setup
- Action
- Assertion
Setup step definitions are used for setting up and rendering the screen(s) under test.
//Sets the current screen in the test case to a given presentable screen
func IAmOnScreen(screen: PresentableScreen)
//Render a given presentable screen from the system under test
func IRender(screen: PresentableScreen)
//Navigate from one screen to another via a set of step definitions
func INavigate(fromScreen: PresentableScreen,
toScreen:PresentableScreen)
Action step defintions are used to interact with the elements using their accessibility attributes (ID, Label, Traits, etc.).
//Touch button with given accessibility identifier
public func ITouchButton(_ buttonId: String)
//Touch button with given accessibility label
public func ITouchButton(withLabel buttonLabel: String)
//Enter text into a text field with a given accessibility identifier
public func IEnterIntoTextField(_ id: String, text: String)
Assertion step defintions are used to verify elements using their accessibility attributes (ID, Label, Traits, etc.).
//Verifies a UIButton exists with the given accessibility identifier
public func IShouldSeeButton(_ buttonId: String)
//Verifies a UIButton exists with the given accessibility label
public func IShouldSeeButton(withLabel buttonLabel: String)
//Verifies a UITextField with a given accessibility identifier contains specific text
public func IShouldSeeTextField(_ textFieldId: String,
withText text: String)
Each screen is Viewable, Assertable, Touchable, Renderable, and Navigable. Each screen includes:
- A reference to the current test case
- A unique trait that is used to identify the screen
- The name of the screen
- A method to render the screen
- A list of entry points to aid in navigation to the screen during flow testing
PresentableScreen is a protocol to use when defining the enum of the presentable screens in the application.
PresentableScreen is an abstraction from the screen object to accomodate workspaces that are broken down into separate feature modules. All presentable screens under test can be defined in a core module even if the screen objects are defined in separate feature modules within the workspace.
ScreenProvider is a class that given a PresentableScreen can return either the Screen type or the Screen object. If a workspace is broken into multiple feature modules, each module will define its own ScreenProvider for the screen objects defined within the module.
ScreenPresenter is a class that implements the registration of the ScreenProviders so all screens are available to the tests.
The class also provides a method to return a specific Screen Provider Object given a Presentable Screen as well as a method to return a specific Screen object given a Presentable Screen.
ScreenRenderer is a protocol for creating a screen from the system under test.
Swerkin is available through CocoaPods.
To install it, simply add the following to your Podfile:
target 'Your Apps' do
...
end
target 'Acceptance Tests' do
pod 'Swerkin'
end
After adding, Run pod install to complete
There are three components that must be implemented
- Test Core Classes
- Screen Object Classes
- Test Classes (aka Features or Specs)
Screen Class
We recommend creating a base screen class that implements the Screen protocol so default values can be provided.
class ExampleBaseScreen: Screen {
final var test: BaseTestCase
final let renderer: ScreenRenderer = ExampleScreenRenderer()
public required init(testCase: BaseTestCase) {
self.test = testCase
}
final var testName: String { return test.name }
var trait: String { return "" }
var name: String { return "" }
func create() -> UIViewController { UIViewController() }
func renderScreen() {}
func entryPathSegments() -> [PathSegment] {
return []
}
}
The other option is each Screen object should implement the Screen protocol.
PresentableScreen Enum
Create an enum with a case for each screen to be tested within the application.
public enum ExamplePresentableScreen: String, PresentableScreen {
case buttonScreen
case dropDownScreen
case endToEndScreen
case homeScreen
case swipeScreen
case tappableScreen
case textFieldScreen
case waitToSeeScreen
case tableViewScreen
public var rawValue: String {
get {
return String(describing: self).capitalized
}
}
}
ScreenProvider Class
Each ScreenProvider class should inherit from ScreenProvider and override the functions screen and typeMarker for the Presentable Screens being translated to their respective Screens.
public class ExampleScreenProvider: ScreenProvider<ExamplePresentableScreen> {
public var testReference: BaseTestCase! = nil
public required init(testCase: BaseTestCase?) {
self.testReference = testCase
}
public override func screen(for screen: ExamplePresentableScreen) -> Screen? {
switch screen {
case .buttonScreen:
return ButtonScreen(testCase: self.testReference)
case .dropDownScreen:
return DropdownScreen(testCase: testReference)
...
}
}
public override func typeMarker(for screen: ExamplePresentableScreen) -> Screen.Type? {
switch screen {
case .buttonScreen:
return ButtonScreen.self
case .dropDownScreen:
return DropdownScreen.self
...
}
}
}
Screen Renderer Class
The ScreenRenderer class should implement the ScreenRender protocol and provide a implementation for the function screen. The function should three key elements:
- Create the screen under test (most likely a ViewController)
- Add the screen under test to the top of the Navigation stack
- Verify a unique element on the screen (trait) appears as expected
class ExampleScreenRenderer: ScreenRenderer {
func screen(_ screenObject: Screen, didRenderWithAuth isAuth: Bool) {
guard let screenObject = screenObject as? ExampleBaseScreen else { return }
if isAuth {
//Add code that is special to your app when the user is authenticated
}
//Navigation code to render the ViewController and add it to the stack to navigate directly to it
if let navigationController = UIApplication.shared.topNavigationController() {
navigationController.pushViewController(screenObject.create(), animated: false)
}
screenObject.viewTester.waitForAnimationsToFinish()
screenObject.waitForElement(withIdentifier: screenObject.trait)
}
}
Test Case Class
Create an Test Case class for your tests that inherits from BaseTestCase. Register all the ScreenProviders in the setup and make sure to reset the test environment in the tearDown so each test can run independently.
When creating the Test Case class the defaults from BaseTestCase can also be overridden.
open class ExampleTestCase: BaseTestCase {
open override func setUp() {
super.setUp()
self.screenPresenter.registerScreenProvider(ExampleScreenProvider(testCase: self), for: ExamplePresentableScreen.self)
}
open override func tearDown() {
resetNavigation {
self.navigateHome()
self.waitForAnimationsToFinish()
}
super.tearDown()
}
...
}
Create screen object classes that inherits from either your base screen object or implements the Screen protocol. There should be a screen object class for each screen being verified within the system under test.
Each screen object should implement the following:
- var trait
- var name
- func create()
- func entryPathSegments()
- func renderScreen()
- enum View: Accessibility
Create test classes that inherit from your application's base test case class. Within the test class, build the test cases using the catalog of step definitions using the Gherkin-like syntax.
class Dropdown: ExampleTestCase {
private let textField = DropdownScreen.View.textField.accessibilityIdentifier
func testVerifySingleItem() {
Given.IAmOnScreen(ExamplePresentableScreen.dropDownScreen)
And.IRender(screen: ExamplePresentableScreen.dropDownScreen)
When.ISetDropDown(textField, toValue: "Banana")
Then.IShouldSeeTextField(textField, withText: "Banana")
}
func testVerifyWithLabelItem() {
Given.IAmOnScreen(ExamplePresentableScreen.dropDownScreen)
And.IRender(screen: ExamplePresentableScreen.dropDownScreen)
When.ISetDropDown(withLabel: "textField", toValue: "Orange")
Then.IShouldSeeTextField(textField, withText: "Orange")
}
...
}
An example application was created as a vehicle to illustrate how to implement the framework. As discussed above in the implementation sections, there are three components that were implemented to write Swerkin tests against the example application:
- Test Core Classes
- Screen Object Classes
- Test Cases
Example test core classes have been added under Swerkin-UITests-Examples/TestCore including:
- ExampleBaseScreen
- ExamplePresentableScreen
- ExampleScreenRenderer
- ExampleScreenProviders
- ExampleTestCase
Examples of screen objects for each ViewController in the example application have been added under Swerkin-UITests-Examples/Screens.
Example test cases for most of the UI elements have been added under Swerkin-UITests-Examples/Features.
Here is an example test for the verification of the text within the first name text field using its accessibility label.
func testVerifyExistenceOfFirstNameTextFieldWithLabel() {
Given.IAmOnScreen(ExamplePresentableScreen.textFieldScreen)
And.IRender(screen: ExamplePresentableScreen.textFieldScreen)
When.IWaitToSeeScreen(ExamplePresentableScreen.textFieldScreen)
Then.IShouldSeeTextField(withLabel: "first name text Field",
withText: "John")
}
To run the example project, clone the repo, and run pod install
from the Example directory first.
Select CMD-U Swerkin_Example scheme to execute the tests.