PlaygroundTester is a package that enables you to add tests to your iPad Swift Playgrounds project.
Installation
Just add PlaygroundTester package to the project as you normally would.
Usage
Adding tests
For PlaygroundTester to find and properly execute tests your test class needs to :
Inherit from TestCase
Be marked as having @objcMembers
Alternatively you can mark each method you want to be discoverable as @objc
At this momment inheriting from another test class is not supported (so you cannot create a class BaseTests: TestCase that you will then inherit other test classes from).
Sample test class declaration :
@objcMembers
final class MyTests: TestCase {
}
setUp / tearDown
You can override four methods to help with setting up and cleaning up after your tests.
// Called once for the entire test class, before any tests are run.
open class func setUp() { }
// Called before each test method.
// If this method throws, the test method won't be executed, but `tearDown` will be.
open func setUp() throws { }
// Called after each test method.
open func tearDown() throws { }
// Called once for the entire test class, after all tests are run.
open class func tearDown() { }
}
Adding test methods
For PlaygroundTester to discover your test methods and run them automatically they have to :
Be non-private (so public or internal)
Begin with test
Any private methods or ones not begininning with test will not be automatically executed, so you can use this opportunity to define helper methods.
Sample method definition :
func testSample() { }
// Methods that won't be run automaticaly
private func testPrivateSample() { }
func helperMethod() { }
Throwing test methods
Throwing test methods are supported by PlaygroundTester - just define your method following the rules above and add throws to its definition.
PlaygroundTester will catch the thrown error and report it.
Sample throwing test method definition :
func testSampleThrowing() throws { }
Asserting
Currently there is a basic set of assertion methods available that mimick the asserting style of XCTest:
// Assert of a passed boolean value is `true`/`false`
public func Assert(_ value: Bool, message: String = "")
public func AssertFalse(_ value: Bool, message: String = "")
// Assert if two passed values are equal / not equal.
public func AssertEqual<T: Equatable>(_ value: T, other: T, message: String = "")
public func AssertNotEqual<T: Equatable>(_ value: T, other: T, message: String = "")
// assert if passed optional value is `nil` / not `nil`.
public func AssertNil<T>(_ value: T?, message: String = "")
public func AssertNotNil<T>(_ value: T?, message: String = "")
There are a few methods missing from achieving parity with XCTAssert family of methods which will be added later.
Unwrapping
PlaygroundTester provides a similar method to XCTUnwrap:
// Return an unwrapped value, or throw an error if `nil` was passed.
public func AssertUnwrap<T>(_ value: T?, message: String = "") throws -> T
You should mark your test method with throws to avoid the need to handle this thrown error yourself.
Sample method with AssertUnwrap :
func testSampleAssertUnwrap() throws {
let sampleArray = ["first", "second"]
let firstValue = try XCTUnwrap(sampleArray.first, "Array should have a first element").
// `firstValue` is non-optional here
}
Expectations
PlaygroundTester supports waiting on expectations to test asynchronous code.
Expectations can be configured with 3 properties :
expectedFulfilmentCount (default == 1) - how many times should the expectation be fullfilled to be considered as met. Expectations will fail if they are overfulfilled.
inverted (default == false) - if the expectation is inverted it will fail, if it is fullfilled.
If you have an inverted expectation with expectedFulfilmentCount > 1 it will be considered as met if it gets fullfilled less than expectedFulfilmentCount times.
You use the AssertExpectations method to wait on created expectations :
// Will wait for `timeout` seconds for `expectations` to be fulfilled before continuing test execution.
public func AssertExpectations(_ expectations: [Expectation], timeout: TimeInterval)
Sample test with expectation :
func testSampleExpectation() {
let expectation = Expectation(name: "Wait for main thread")
DispatchQueue.main.async {
expectation.fulfill()
}
AssertExpectations([expectation], timeout: 2)
}
At this moment unwaited expectations don’t trigger an assertion failure.
Runing tests
In order to execute your tests you need to do one final thing : Wrap your view in PlaygroundTesterWrapperView and set PlaygroundTester.PlaygroundTesterConfiguration.isTesting flag to true.
In your App object just do this :
struct Myapp: App {
init() {
PlaygroundTester.PlaygroundTesterConfiguration.isTesting = true
}
var body: some Scene {
WindowGroup {
PlaygroundTester.PlaygroundTesterWrapperView {
// YourContentView()
}
}
}
}
After that when running the app either in fullscreen or in preview mode will instead discover and run your tests.
Inspecting results
After the tests are run, you can navigate them to inspect their results and see which assertions failed.
Swift Playgrounds doesn’t support multiple targets at this time, so your test files will need to be kept alongside regular application code.
The package itself has compilation guards around its code, so that when creating a release build most of it will be omitted from you production app.
What is left is the minimal set of object and function definitions, so that your code compiles just fine, and all calls to PlaygroundTester provided
objects/functions resolves to basically no-ops.
If you’d like to also discard your tests from release builds, you’ll need to add a compilation flag to your app.
For now you’ll need to follow these steps to add a compilation flag to your project :
Send the app project to a Mac (for example via AirDrop)
Open the package contents (right click -> Show Package Contents)
Open Package.swift file
This file should contain a single .executableTarget definition.
Add this argument to the target : swiftSettings: [.define("TESTING_ENABLED", .when(configuration: .debug))]
Save the file and share the app back to your iPad.
In the end the target definition should look similar to this :
targets: [
.executableTarget(
name: "AppModule",
dependencies: [
// if any
],
path: ".",
swiftSettings: [.define("TESTING_ENABLED", .when(configuration: .debug))]
)
]
PlaygroundTester
PlaygroundTester
is a package that enables you to add tests to your iPad Swift Playgrounds project.Installation
Just add
PlaygroundTester
package to the project as you normally would.Usage
Adding tests
For
PlaygroundTester
to find and properly execute tests your test class needs to :TestCase
@objcMembers
@objc
At this momment inheriting from another test class is not supported (so you cannot create a
class BaseTests: TestCase
that you will then inherit other test classes from).Sample test class declaration :
setUp / tearDown
You can override four methods to help with setting up and cleaning up after your tests.
Adding test methods
For
PlaygroundTester
to discover your test methods and run them automatically they have to :private
(sopublic
orinternal
)test
Any
private
methods or ones not begininning withtest
will not be automatically executed, so you can use this opportunity to define helper methods.Sample method definition :
Throwing test methods
Throwing test methods are supported by
PlaygroundTester
- just define your method following the rules above and addthrows
to its definition.PlaygroundTester
will catch the thrown error and report it.Sample throwing test method definition :
Asserting
Currently there is a basic set of assertion methods available that mimick the asserting style of
XCTest
:There are a few methods missing from achieving parity with
XCTAssert
family of methods which will be added later.Unwrapping
PlaygroundTester
provides a similar method toXCTUnwrap
:You should mark your test method with
throws
to avoid the need to handle this thrown error yourself.Sample method with
AssertUnwrap
:Expectations
PlaygroundTester
supports waiting on expectations to test asynchronous code. Expectations can be configured with 3 properties :expectedFulfilmentCount (default == 1)
- how many times should the expectation be fullfilled to be considered as met. Expectations will fail if they are overfulfilled.inverted (default == false)
- if the expectation isinverted
it will fail, if it is fullfilled.inverted
expectation withexpectedFulfilmentCount > 1
it will be considered as met if it gets fullfilled less thanexpectedFulfilmentCount
times.You use the
AssertExpectations
method to wait on created expectations :Sample test with expectation :
At this moment unwaited expectations don’t trigger an assertion failure.
Runing tests
In order to execute your tests you need to do one final thing : Wrap your view in PlaygroundTesterWrapperView and set PlaygroundTester.PlaygroundTesterConfiguration.isTesting flag to true. In your
App
object just do this :After that when running the app either in fullscreen or in preview mode will instead discover and run your tests.
Inspecting results
After the tests are run, you can navigate them to inspect their results and see which assertions failed.
https://user-images.githubusercontent.com/4209155/154171145-2387477e-a665-4991-b63e-3f2dfe3cad73.mp4
Patching
Package.swift
Swift Playgrounds doesn’t support multiple targets at this time, so your test files will need to be kept alongside regular application code. The package itself has compilation guards around its code, so that when creating a release build most of it will be omitted from you production app. What is left is the minimal set of object and function definitions, so that your code compiles just fine, and all calls to
PlaygroundTester
provided objects/functions resolves to basically no-ops.If you’d like to also discard your tests from release builds, you’ll need to add a compilation flag to your app.
For now you’ll need to follow these steps to add a compilation flag to your project :
Package.swift
file.executableTarget
definition.swiftSettings: [.define("TESTING_ENABLED", .when(configuration: .debug))]
In the end the target definition should look similar to this :
You can of course choose any name for the flag.
NOTE : I hope to automate this process in Patching Package.swift
Supported features
setUp
/tearDown
methodsRoadmap
Things I’d like to explore and add to
PlaygroundTester
(random order):Package.swift
patchingXCTest
async
/await
Combine
Please check Issues for more info.