Skip to content

Commit

Permalink
feat: re-add test, modify CI
Browse files Browse the repository at this point in the history
  • Loading branch information
hilmyveradin committed Aug 14, 2024
1 parent 7beab71 commit a867b12
Show file tree
Hide file tree
Showing 10 changed files with 365 additions and 51 deletions.
61 changes: 23 additions & 38 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,33 @@ name: OTPKitTests

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

jobs:
build:
runs-on: macos-latest

steps:
- uses: actions/checkout@v2

- name: Switch Xcode 15
run: sudo xcode-select -switch /Applications/Xcode_15.0.1.app

- name: Install xcodegen
run: brew install xcodegen

- name: Generate xcodeproj for OTPKit
run: xcodegen

# Build
- name: Build OneBusAway
run: xcodebuild clean build-for-testing
-scheme 'OTPKitDemo'
-destination 'platform=iOS Simulator,name=iPhone 15'
-quiet

# Unit Test
- name: OBAKit Unit Test
run: xcodebuild test-without-building
-only-testing:OTPKitTests
-project 'OTPKit.xcodeproj'
-scheme 'OTPKitDemo'
-destination 'platform=iOS Simulator,name=iPhone 15'
-resultBundlePath OTPKitTests.xcresult
-quiet

# Upload results
- uses: kishikawakatsumi/[email protected]
continue-on-error: true
with:
show-passed-tests: false # Avoid truncation of annotations by GitHub by omitting succeeding tests.
path: |
OTPKitTests.xcresult
if: success() || failure()
- uses: actions/checkout@v2

- name: Switch Xcode 15
run: sudo xcode-select -switch /Applications/Xcode_15.0.1.app

# Build
- name: Build OTPKit
run: |
xcodebuild build-for-testing \
-scheme OTPKit \
-destination "platform=iOS Simulator,name=iPhone 15,OS=latest" \
-enableCodeCoverage YES
# Upload results
- uses: kishikawakatsumi/[email protected]
continue-on-error: true
with:
show-passed-tests: false # Avoid truncation of annotations by GitHub by omitting succeeding tests.
path: |
OTPKitTests.xcresult
if: success() || failure()
7 changes: 7 additions & 0 deletions Examples/OTPKitDemo/OTPKitDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0111FD712C6CFBE400B4472E /* OTPKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0111FD702C6CFBE400B4472E /* OTPKit */; };
014316DF2C6B6F2C00B33240 /* OTPKit in Frameworks */ = {isa = PBXBuildFile; productRef = 014316DE2C6B6F2C00B33240 /* OTPKit */; };
01AA80542C6B6A7500D4038A /* OTPKitDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AA80532C6B6A7500D4038A /* OTPKitDemoApp.swift */; };
01AA80562C6B6A7500D4038A /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AA80552C6B6A7500D4038A /* MapView.swift */; };
Expand All @@ -29,6 +30,7 @@
buildActionMask = 2147483647;
files = (
014316DF2C6B6F2C00B33240 /* OTPKit in Frameworks */,
0111FD712C6CFBE400B4472E /* OTPKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -89,6 +91,7 @@
name = OTPKitDemo;
packageProductDependencies = (
014316DE2C6B6F2C00B33240 /* OTPKit */,
0111FD702C6CFBE400B4472E /* OTPKit */,
);
productName = OTPKitDemo;
productReference = 01AA80502C6B6A7500D4038A /* OTPKitDemo.app */;
Expand Down Expand Up @@ -365,6 +368,10 @@
/* End XCLocalSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
0111FD702C6CFBE400B4472E /* OTPKit */ = {
isa = XCSwiftPackageProductDependency;
productName = OTPKit;
};
014316DE2C6B6F2C00B33240 /* OTPKit */ = {
isa = XCSwiftPackageProductDependency;
productName = OTPKit;
Expand Down
9 changes: 8 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ let package = Package(
name: "OTPKit"),
.testTarget(
name: "OTPKitTests",
dependencies: ["OTPKit"])
dependencies: ["OTPKit"],
path: "Tests",
resources: [
// Copy Tests/ExampleTests/Resources directories as-is.
// Use to retain directory structure.
// Will be at top level in bundle.
.process("Resources"),

Check warning on line 31 in Package.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Comma Violation: Collection literals should not have trailing commas (trailing_comma)
]),

Check warning on line 32 in Package.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Comma Violation: Collection literals should not have trailing commas (trailing_comma)
]
)
55 changes: 55 additions & 0 deletions Tests/Helpers/Fixtures.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright (C) Open Transit Software Foundation
*
* 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
@testable import OTPKit

class Fixtures {
private class var testBundle: Bundle {
Bundle.module
}

/// Converts the specified dictionary to a model object of type `T`.
/// - Parameters:
/// - type: The model type to which the dictionary will be converted.
/// - dictionary: The data
/// - Returns: A model object
class func dictionaryToModel<T>(type: T.Type, dictionary: [String: Any]) throws -> T where T: Decodable {
let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
return try JSONDecoder().decode(type, from: jsonData)
}

/// Returns the path to the specified file in the test bundle.
/// - Parameter fileName: The file name, e.g. "regions.json"
class func path(to fileName: String) -> String {
testBundle.path(forResource: fileName, ofType: nil)!
}

/// Encodes and decodes the provided `Codable` object. Useful for testing roundtripping.
/// - Parameter type: The object type.
/// - Parameter model: The object or objects.
class func roundtripCodable<T>(type: T.Type, model: T) throws -> T where T: Codable {
let encoded = try PropertyListEncoder().encode(model)
let decoded = try PropertyListDecoder().decode(type, from: encoded)
return decoded
}

/// Loads data from the specified file name, searching within the test bundle.
/// - Parameter file: The file name to load data from. Example: `stop_data.pb`.
class func loadData(file: String) -> Data {
NSData(contentsOfFile: path(to: file))! as Data
}
}
148 changes: 148 additions & 0 deletions Tests/Helpers/MockDataLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright (C) Open Transit Software Foundation
*
* 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
import OTPKit

typealias MockDataLoaderMatcher = (URLRequest) -> Bool

struct MockDataResponse {
let data: Data?
let urlResponse: URLResponse?
let error: Error?
let matcher: MockDataLoaderMatcher
}

class MockTask: URLSessionDataTask {
override var progress: Progress {
Progress()
}

private var closure: (Data?, URLResponse?, Error?) -> Void
private let mockResponse: MockDataResponse

init(mockResponse: MockDataResponse, closure: @escaping (Data?, URLResponse?, Error?) -> Void) {
self.mockResponse = mockResponse
self.closure = closure
}

// We override the 'resume' method and simply call our closure
// instead of actually resuming any task.
override func resume() {
closure(mockResponse.data, mockResponse.urlResponse, mockResponse.error)
}

override func cancel() {
// nop
}
}

class MockDataLoader: NSObject, URLDataLoader {
var mockResponses = [MockDataResponse]()

let testName: String

init(testName: String) {
self.testName = testName
}

func dataTask(
with request: URLRequest,
completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void
) -> URLSessionDataTask {
guard let response = matchResponse(to: request) else {
fatalError("\(testName): Missing response to URL: \(request.url!)")
}

return MockTask(mockResponse: response, closure: completionHandler)
}

func data(for request: URLRequest) async throws -> (Data, URLResponse) {
guard let response = matchResponse(to: request) else {
fatalError("\(testName): Missing response to URL: \(request.url!)")
}

if let error = response.error {
throw error
}

guard let data = response.data else {
fatalError("\(testName): Missing data to URL: \(request.url!))")
}

guard let urlResponse = response.urlResponse else {
fatalError("\(testName): Missing urlResponse to URL: \(request.url!))")
}

return (data, urlResponse)
}

// MARK: - Response Mapping

func matchResponse(to request: URLRequest) -> MockDataResponse? {
for r in mockResponses where r.matcher(request) {
return r
}

return nil
}

func mock(data: Data, matcher: @escaping MockDataLoaderMatcher) {
let urlResponse = buildURLResponse(URL: URL(string: "https://mockdataloader.example.com")!, statusCode: 200)
let mockResponse = MockDataResponse(data: data, urlResponse: urlResponse, error: nil, matcher: matcher)
mock(response: mockResponse)
}

func mock(URLString: String, with data: Data) {
mock(url: URL(string: URLString)!, with: data)
}

func mock(url: URL, with data: Data) {
let urlResponse = buildURLResponse(URL: url, statusCode: 200)
let mockResponse = MockDataResponse(data: data, urlResponse: urlResponse, error: nil) {
let requestURL = $0.url!
return requestURL.host == url.host && requestURL.path == url.path
}
mock(response: mockResponse)
}

func mock(response: MockDataResponse) {
mockResponses.append(response)
}

func removeMappedResponses() {
mockResponses.removeAll()
}

// MARK: - URL Response

func buildURLResponse(URL: URL, statusCode: Int) -> HTTPURLResponse {
HTTPURLResponse(
url: URL,
statusCode: statusCode,
httpVersion: "2",
headerFields: ["Content-Type": "application/json"]
)!
}

// MARK: - Description

override var debugDescription: String {
var descriptionBuilder = DebugDescriptionBuilder(baseDescription: super.debugDescription)
descriptionBuilder.add(key: "mockResponses", value: mockResponses)
return descriptionBuilder.description
}
}
50 changes: 50 additions & 0 deletions Tests/Helpers/OTPTestCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// OTPTestCase.swift
// OTPKitTests
//
// Created by Aaron Brethorst on 5/2/24.
//

import Foundation
@testable import OTPKit
import XCTest

public class OTPTestCase: XCTestCase {
var userDefaults: UserDefaults!

override open func setUp() {
super.setUp()
NSTimeZone.default = NSTimeZone(forSecondsFromGMT: 0) as TimeZone
userDefaults = buildUserDefaults()
userDefaults.removePersistentDomain(forName: userDefaultsSuiteName)
}

override open func tearDown() {
super.tearDown()
NSTimeZone.resetSystemTimeZone()
userDefaults.removePersistentDomain(forName: userDefaultsSuiteName)
}

// MARK: - User Defaults

func buildUserDefaults(suiteName: String? = nil) -> UserDefaults {
UserDefaults(suiteName: suiteName ?? userDefaultsSuiteName)!
}

var userDefaultsSuiteName: String {
String(describing: self)
}

// MARK: - Network and Data

func buildMockDataLoader() -> MockDataLoader {
MockDataLoader(testName: name)
}

func buildRestAPIClient(
baseURLString: String = "https://otp.prod.sound.obaweb.org/otp/routers/default/"
) -> RestAPI {
let baseURL = URL(string: baseURLString)!
return RestAPI(baseURL: baseURL, dataLoader: buildMockDataLoader())
}
}
Loading

0 comments on commit a867b12

Please sign in to comment.