Using a test suite for automation
16 May 2021Have you ever needed to automate a task that needs to be run on an iOS simulator and not known quite how to make it work in an unsupervised way? As an example - imagine we need to take some JSON files, run each one of them through an app and capture the generated UI so that the screenshots can be uploaded for further processing.
At a high level we want an executable that can take a directory full of input, run a task inside the simulator and place the output of all of the tasks into a different directory. We’ll start by looking at building something to run our task inside a simulator and then see how to get data in/out of the process.
Running our task
To run our code inside the simulator we can make use of a new test suite with a single test method. The test method will enumerate files found at the input directory path, execute some code on the simulator and store the results in a different directory. Here’s a basic implementation of the above:
import XCTest
class Processor: XCTestCase {
func testExample() throws {
let (inputDirectory, outputDirectory) = try readDirectoryURLs()
let fileManager = FileManager.default
try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
try fileManager.contentsOfDirectory(at: inputDirectory, includingPropertiesForKeys: nil).forEach {
try eval($0).write(to: outputDirectory.appendingPathComponent($0.lastPathComponent))
}
}
private func readDirectoryURLs() throws -> (input: URL, output: URL) {
func read(_ path: String) throws -> URL {
URL(fileURLWithPath: try String(contentsOf: Bundle(for: Processor.self).bundleURL.appendingPathComponent("\(path)-directory-path")).trimmingCharacters(in: .whitespacesAndNewlines))
}
return try (read("input"), read("output"))
}
}
The interesting things to note above are:
- The input/output directory paths need to be written to files called
input-directory-path
andoutput-directory-path
inside the test bundle - The
eval
function is a function that can read the contents of a file and return a result that we can write to the output directory - this is where all of the real work would happen
There are plenty of things that could be done to customise the above for individual use cases but it’s enough for this post.
How do we set up the input-directory-path
and output-directory-path
files inside the test bundle?
Wrapping the task
In order to inject the relevant paths we need to ensure that our test suite is built and then run as two separate steps. This gives us a chance to build the project, inject our file paths and then actually execute the test.
A Ruby script to do this would look something like the following:
#!/usr/bin/env ruby
unless ARGV.count == 2
puts "USAGE: #{$PROGRAM_NAME} INPUT_DIRECTORY OUTPUT_DIRECTORY"
exit 1
end
input_directory, output_directory = *ARGV
def xcode_build mode
`xcodebuild #{mode} -scheme Processor -destination 'platform=iOS Simulator,name=iPhone 12,OS=14.5' -derivedDataPath ./.build`
end
xcode_build "build-for-testing"
Dir[".build/**/Processor.app"].each do |path|
write = -> name, contents do
File.open("#{path}/PlugIns/ProcessorTests.xctest/#{name}-directory-path", 'w') do |file| file.puts contents end
end
write["input", input_directory]
write["output", output_directory]
end
xcode_build "test-without-building"
This script is doing the following:
- Basic input validation to ensure that both an input and output path have been provided
- Run
xcodebuild
with the action ofbuild-for-testing
to ensure that the test suite is built and not run - Write the
input-directory-path
andoutput-directory-path
files into the test bundle - Run
xcodebuild
with the action oftest-without-building
to execute the test suite
With all of these pieces in place and assuming we named this script run-processor
we can execute this script like this:
./run-processor /path/to/input /path/to/output
Result
We have a pretty bare bones implementation that should demonstrate the general idea and leave plenty of scope for expansion and experimentation.