Skip to content

Commit

Permalink
Merge pull request #19 from treatwell/Add-try-catch-mechanics
Browse files Browse the repository at this point in the history
Support try - catch in tests
  • Loading branch information
lokatorius authored Jul 15, 2020
2 parents f22f1ee + c843255 commit fd105d0
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 80 deletions.
4 changes: 2 additions & 2 deletions ExampleUITests/Home/HomeUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import Foundation
import TWUITests

final class HomeTests: UITestCase {
func testAPIResponse() {
start(using: Configuration().isUser("[email protected]", password: "password")) { app in
func testAPIResponse() throws {
try start(with: Configuration().isUser("[email protected]", password: "password")) { app in
app.replaceValues(
of: [
"result": "AUTHENTICATED"
Expand Down
16 changes: 8 additions & 8 deletions ExampleUITests/Login/LoginUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,31 @@ import XCTest

final class LoginUITests: UITestCase {

func testFirstTimeUserLoginScreenIsVisible() {
start(using: Configuration()
func testFirstTimeUserLoginScreenIsVisible() throws {
try start(with: Configuration()
.isFirstTimeUser()
)
.loginStep.loginScreenIsVisible()
}

func testEmptyUsernameOrPasswordShowsAlert() {
start(using: Configuration()
func testEmptyUsernameOrPasswordShowsAlert() throws {
try start(with: Configuration()
.isFirstTimeUser()
)
.loginStep.providesEmptyCredentials()
.loginStep.errorAlertIsVisible()
}

func testThatUserCanLogin() {
start(using: Configuration()
func testThatUserCanLogin() throws {
try start(with: Configuration()
.isFirstTimeUser()
)
.loginStep.providesUsername("[email protected]", password: "password")
.homeStep.homeScreenIsVisible()
}

func testAPIResponse() {
start(using: Configuration()) { app in
func testAPIResponse() throws {
try start(with: Configuration()) { app in
app.replaceValues(
of: [
"result": "NOT_AUTHENTICATED"
Expand Down
16 changes: 16 additions & 0 deletions TWUITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
1F1628A223378D4E002DE2BF /* ReplacementJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1628A123378D4E002DE2BF /* ReplacementJob.swift */; };
1F1628A423378D7C002DE2BF /* HttpServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1628A323378D7C002DE2BF /* HttpServerProtocol.swift */; };
1F5AD060233A1D3D0071E01E /* PortSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5AD05F233A1D3D0071E01E /* PortSettingsTests.swift */; };
1F7F2EA224BDD439003BF22F /* FileManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7F2EA124BDD439003BF22F /* FileManaging.swift */; };
1FBA39AE2293FEEC0034DF26 /* APIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FBA39AD2293FEEC0034DF26 /* APIConfiguration.swift */; };
1FE86220228D7197003EA159 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D2FF8B227319DE008B37F4 /* AccessibilityIdentifiers.swift */; };
1FFB048024BDB97D00E518D5 /* HTTPDynamicStubsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFB047F24BDB97D00E518D5 /* HTTPDynamicStubsTests.swift */; };
293F1D19248F815800EC03C4 /* APIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 293F1D18248F815800EC03C4 /* APIProvider.swift */; };
70D2FF1B2271F9C7008B37F4 /* TWUITests.h in Headers */ = {isa = PBXBuildFile; fileRef = 70D2FF192271F9C7008B37F4 /* TWUITests.h */; settings = {ATTRIBUTES = (Public, ); }; };
70D2FF282271F9D4008B37F4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D2FF272271F9D4008B37F4 /* AppDelegate.swift */; };
Expand Down Expand Up @@ -81,7 +83,9 @@
1F1628A123378D4E002DE2BF /* ReplacementJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplacementJob.swift; sourceTree = "<group>"; };
1F1628A323378D7C002DE2BF /* HttpServerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpServerProtocol.swift; sourceTree = "<group>"; };
1F5AD05F233A1D3D0071E01E /* PortSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortSettingsTests.swift; sourceTree = "<group>"; };
1F7F2EA124BDD439003BF22F /* FileManaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManaging.swift; sourceTree = "<group>"; };
1FBA39AD2293FEEC0034DF26 /* APIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIConfiguration.swift; sourceTree = "<group>"; };
1FFB047F24BDB97D00E518D5 /* HTTPDynamicStubsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPDynamicStubsTests.swift; sourceTree = "<group>"; };
293F1D18248F815800EC03C4 /* APIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIProvider.swift; sourceTree = "<group>"; };
70D2FF162271F9C7008B37F4 /* TWUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TWUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
70D2FF192271F9C7008B37F4 /* TWUITests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TWUITests.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -168,6 +172,7 @@
03A1579F22FD66B700A60090 /* Info.plist */,
03A157A622FD952C00A60090 /* RegexJSONModifierTests.swift */,
1F5AD05F233A1D3D0071E01E /* PortSettingsTests.swift */,
1FFB047F24BDB97D00E518D5 /* HTTPDynamicStubsTests.swift */,
);
path = TWUITestsTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -254,6 +259,7 @@
03A157A822FD954E00A60090 /* RegexJSONModifier.swift */,
1F1628A123378D4E002DE2BF /* ReplacementJob.swift */,
1F1628A323378D7C002DE2BF /* HttpServerProtocol.swift */,
1F7F2EA124BDD439003BF22F /* FileManaging.swift */,
);
path = APIStubs;
sourceTree = "<group>";
Expand Down Expand Up @@ -564,6 +570,7 @@
files = (
03A157A722FD952C00A60090 /* RegexJSONModifierTests.swift in Sources */,
1F5AD060233A1D3D0071E01E /* PortSettingsTests.swift in Sources */,
1FFB048024BDB97D00E518D5 /* HTTPDynamicStubsTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -586,6 +593,7 @@
70D2FF4F227202D7008B37F4 /* HTTPStubsList.swift in Sources */,
70D2FF672272EBF4008B37F4 /* XCUIElement+Extensions.swift in Sources */,
1FBA39AE2293FEEC0034DF26 /* APIConfiguration.swift in Sources */,
1F7F2EA224BDD439003BF22F /* FileManaging.swift in Sources */,
70D2FF5B2272EA63008B37F4 /* UITestBase.swift in Sources */,
70D2FF6B2272EC24008B37F4 /* XCUIElementQuery+Extension.swift in Sources */,
70D2FF592272E9F9008B37F4 /* UITestCase.swift in Sources */,
Expand Down Expand Up @@ -664,6 +672,10 @@
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = X5394XCRFK;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
INFOPLIST_FILE = TWUITestsTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -683,6 +695,10 @@
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = X5394XCRFK;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Carthage/Build/iOS",
);
INFOPLIST_FILE = TWUITestsTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
Expand Down
23 changes: 23 additions & 0 deletions TWUITests/APIStubs/FileManaging.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2019 Hotspring Ventures Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

protocol FileManaging {
func createDirectory(at url: URL, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey: Any]?) throws
func fileExists(atPath path: String) -> Bool
func contents(atPath path: String) -> Data?
}

extension FileManager: FileManaging {}
134 changes: 92 additions & 42 deletions TWUITests/APIStubs/HTTPDynamicStubs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,44 @@ import Swifter

protocol HTTPDynamicStubing {
func update(with stubInfo: APIStubInfo)
@available(*, deprecated, message: "Use throwable `startServer()` instead")
func start() -> UInt16
func startServer() throws -> UInt16
func stop()
func replace(with: ReplacementJob)
}

final class HTTPDynamicStubs: HTTPDynamicStubing {
private let fileManager: FileManager
enum Error: Swift.Error {
case fileDoesNotExist(String)
case dataDoesNotExist(String)
case jsonDecode(String)
case cantGetSimulatorSharedDir
}
private let fileManager: FileManaging
private let server: HttpServerProtocol
private let appID: String
private let regexModifier: RegexJSONModifier
private var portSettings: PortSettings

init(
fileManager: FileManager = .default,
fileManager: FileManaging = FileManager.default,
server: HttpServerProtocol = HttpServer(),
initialStubs: [APIStubInfo] = HTTPDynamicStubsList().initialStubs,
regexModifier: RegexJSONModifier = RegexJSONModifier(),
appID: String,
port: APIConfiguration.PortType,
maxPortRetries: Int = 5
) {
) throws {
self.fileManager = fileManager
self.server = server
self.appID = appID
self.regexModifier = regexModifier
self.portSettings = PortSettings(port: port, maxRetriesCount: maxPortRetries)
setup(initialStubs: initialStubs)
try setup(initialStubs: initialStubs)
}

@available(*, deprecated, message: "Use throwable `startServer()` instead")
func start() -> UInt16 {
do {
try server.start(portSettings.port)
Expand All @@ -57,104 +66,145 @@ final class HTTPDynamicStubs: HTTPDynamicStubing {
else {
showError("Failed to start local server after \(portSettings.maxRetriesCount) retries. \(error.localizedDescription)")
}
portSettings.retry()
try? portSettings.retry()
return start()
} catch {
showError("Failed to start local server \(error.localizedDescription)")
}
}

func startServer() throws -> UInt16 {
do {
try server.start(portSettings.port)
return portSettings.port
} catch let error as SocketError {
guard
case .bindFailed = error,
portSettings.canRetry
else {
print("Failed to start local server after \(portSettings.maxRetriesCount) retries. \(error.localizedDescription)")
throw error
}
try portSettings.retry()
return try startServer()
} catch let error {
print("Failed to start local server \(error.localizedDescription)")
throw error
}
}

func stop() {
server.stop()
}

@available(*, deprecated, message: "Use throwable `update(using:)` instead")
func update(with stubInfo: APIStubInfo) {
setupStub(stubInfo)
try? setupStub(stubInfo)
}

func update(using stubInfo: APIStubInfo) throws {
try setupStub(stubInfo)
}

@available(*, deprecated, message: "Use throwable `replace(using:)` instead")
func replace(with job: ReplacementJob) {
transform({
try? transform({
try self.regexModifier.apply(modification: job.modification, in: $0)
},
in: job.stub
)
}

func replace(using job: ReplacementJob) throws {
try transform({
try self.regexModifier.apply(modification: job.modification, in: $0)
},
in: job.stub
)
}

private func transform(_ modifyFn: (Data) throws -> Data, in stub: APIStubInfo) {
private func transform(_ modifyFn: (Data) throws -> Data, in stub: APIStubInfo) throws {
do {
let dataObject = getDataObject(from: stub)
guard let json = dataToJSON(data: dataObject) else { return }
let dataObject = try getDataObject(from: stub)
guard let json = try dataToJSON(data: dataObject) else { return }
let data = try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted)
stubJSON(object: try modifyFn(data), for: stub)
try stubJSON(object: try modifyFn(data), for: stub)
} catch let error {
print(error)
print("Transform error: \(error)")
throw error
}
}

private var stubsDirectory: URL {
private func stubsDirectory() throws -> URL {
guard let simulatorSharedDir = ProcessInfo().environment["SIMULATOR_SHARED_RESOURCES_DIRECTORY"] else {
showError("Cannot get Caches directory")
print("Cannot get Caches directory")
throw Error.cantGetSimulatorSharedDir
}
let simulatorHomeDirURL = URL(fileURLWithPath: simulatorSharedDir)
let cachesDirURL = simulatorHomeDirURL.appendingPathComponent("Library/Caches")
let sharedAPIStubsDirURL = cachesDirURL.appendingPathComponent("ApiStubs", isDirectory: true)
let finalSharedAPIStubsDirURL: URL = appID.isEmpty
let cacheDirPath = "Library/Caches"
let sharedAPIStubsDir = "ApiStubs"

let cachesDirURL = URL(fileURLWithPath: simulatorSharedDir).appendingPathComponent(cacheDirPath)
let sharedAPIStubsDirURL = cachesDirURL.appendingPathComponent(sharedAPIStubsDir, isDirectory: true)
let appSharedAPIStubsDirURL: URL = appID.isEmpty
? sharedAPIStubsDirURL
: sharedAPIStubsDirURL.appendingPathComponent("\(appID)", isDirectory: true)
do {
try fileManager.createDirectory(at: finalSharedAPIStubsDirURL, withIntermediateDirectories: true, attributes: nil)
} catch {
showError("Failed to create shared folder \(finalSharedAPIStubsDirURL.lastPathComponent) in simulator Caches directory at \(cachesDirURL)")
try fileManager.createDirectory(at: appSharedAPIStubsDirURL, withIntermediateDirectories: true, attributes: nil)
} catch let error {
print("Failed to create shared folder \(appSharedAPIStubsDirURL.lastPathComponent) in simulator Caches directory at \(cachesDirURL)")
throw error
}

return finalSharedAPIStubsDirURL
return appSharedAPIStubsDirURL
}

private func setup(initialStubs: [APIStubInfo]) {
private func setup(initialStubs: [APIStubInfo]) throws {
for stub in initialStubs {
setupStub(stub)
try setupStub(stub)
}
}

private func getDataObject(from stub: APIStubInfo) -> Data {
var directory = stubsDirectory
private func getDataObject(from stub: APIStubInfo) throws -> Data {
var directory = try stubsDirectory()
directory.appendPathComponent(stub.jsonFilename + ".json")
let filePath = directory.path

guard fileManager.fileExists(atPath: filePath) else {
showError("File does not exist: \(filePath)")
print("File does not exist: \(filePath)")
throw Error.fileDoesNotExist(filePath)
}

guard let data = fileManager.contents(atPath: filePath) else {
showError("Data does not exist: \(filePath)")
print("Data does not exist: \(filePath)")
throw Error.dataDoesNotExist(filePath)
}

return data
}

private func setupStub(_ stub: APIStubInfo) {
let data = getDataObject(from: stub)
stubJSON(object: data, for: stub)
private func setupStub(_ stub: APIStubInfo) throws {
let data = try getDataObject(from: stub)
try stubJSON(object: data, for: stub)
}

private func stubJSON(object data: Data, for stub: APIStubInfo) {
let response = createResponse(object: data, for: stub)
private func stubJSON(object data: Data, for stub: APIStubInfo) throws {
let response = try createResponse(object: data, for: stub)
switch stub.method {
case .GET :
server.GET[stub.url] = response
server.methodGET(path: stub.url, response: response)
case .POST:
server.POST[stub.url] = response
server.methodPOST(path: stub.url, response: response)
case .PUT:
server.PUT[stub.url] = response
server.methodPUT(path: stub.url, response: response)
case .DELETE:
server.DELETE[stub.url] = response
server.methodDELETE(path: stub.url, response: response)
}
}

private func createResponse(object data: Data?, for stub: APIStubInfo) -> ((HttpRequest) -> HttpResponse) {
private func createResponse(object data: Data?, for stub: APIStubInfo) throws -> ((HttpRequest) -> HttpResponse) {
var json: AnyObject?
if let jsonData = data {
json = dataToJSON(data: jsonData) as AnyObject
json = try dataToJSON(data: jsonData) as AnyObject
}
// Swifter makes it very easy to create stubbed responses
let response: ((HttpRequest) -> HttpResponse) = { _ in
Expand Down Expand Up @@ -182,13 +232,13 @@ final class HTTPDynamicStubs: HTTPDynamicStubing {
return response
}

private func dataToJSON(data: Data) -> Any? {
private func dataToJSON(data: Data) throws -> Any? {
do {
return try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
} catch let myJSONError {
print(myJSONError)
print("JSON serialization error: \(myJSONError)")
throw myJSONError
}
return nil
}

private func showError(_ message: String) -> Never {
Expand Down
Loading

0 comments on commit fd105d0

Please sign in to comment.