├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── KeyPathTesting.podspec
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── KeyPathTesting
│ ├── Assert
│ ├── Assertion.swift
│ ├── AssertionBuilder.swift
│ └── TopLevelFunctions.swift
│ └── Operators
│ └── Operators.swift
└── Tests
└── KeyPathTestingTests
└── OperatorsTests.swift
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: macos-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: fwal/setup-swift@v1
13 | with:
14 | swift-version: "5.4"
15 | - name: Build
16 | run: swift build
17 | - name: Run tests
18 | run: swift test
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | # Package.resolved
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/Build
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
64 |
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots/**/*.png
68 | fastlane/test_output
69 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/KeyPathTesting.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'KeyPathTesting'
3 | s.version = '0.1.5'
4 | s.summary = 'KeyPathTesting is a framework that leverages Result Builders to implement a new syntax that allows developers to write KeyPath-based unit tests'
5 |
6 | s.description = <<~DESC
7 | KeyPathTesting is a Swift framework that leverages Result Builders to implement a new syntax that allows developers to write KeyPath-based unit tests.
8 |
9 | Here's an example of how it can be used:
10 |
11 | class MyTests: XCTestCase {
12 | func test() {
13 | let myData = [1, 2, 3, 4]
14 |
15 | assert(on: myData) {
16 | \.isEmpty == false
17 | \.count > 2
18 | \.first == 1
19 | \.last != 1
20 | }
21 | }
22 | }
23 | DESC
24 |
25 | s.homepage = 'https://github.com/vincent-pradeilles/KeyPathTesting'
26 | s.license = { type: 'MIT', file: 'LICENSE' }
27 | s.author = { 'Vincent Pradeilles' => 'vin.pradeilles+keypathtesting@gmail.com' }
28 | s.source = { git: 'https://github.com/vincent-pradeilles/KeyPathTesting.git', tag: s.version.to_s }
29 |
30 | s.swift_version = '5.4'
31 |
32 | s.ios.deployment_target = '9.0'
33 | s.osx.deployment_target = '10.10'
34 |
35 | s.framework = 'Foundation'
36 | s.framework = 'XCTest'
37 |
38 | s.source_files = 'Sources/KeyPathTesting/**/*.swift'
39 | end
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Vincent Pradeilles
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.4
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "KeyPathTesting",
8 | platforms: [
9 | .iOS(.v9),
10 | .macOS(.v10_10)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "KeyPathTesting",
16 | targets: ["KeyPathTesting"]
17 | ),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "KeyPathTesting",
24 | dependencies: []
25 | ),
26 | .testTarget(
27 | name: "KeyPathTestingTests",
28 | dependencies: ["KeyPathTesting"]
29 | ),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KeyPathTesting
2 |
3 | 
4 | 
5 | [](https://github.com/apple/swift-package-manager)
6 |
7 | ## Context
8 |
9 | KeyPathTesting is a Swift framework that leverages Result Builders to implement a new syntax that allows developers to write KeyPath-based unit tests.
10 |
11 | Here's an example of how it can be used:
12 |
13 | ```swift
14 | class MyTests: XCTestCase {
15 | func test() {
16 | let myData = [1, 2, 3, 4]
17 |
18 | assert(on: myData) {
19 | \.isEmpty == false
20 | \.count > 2
21 | \.first == 1
22 | \.last != 1
23 | }
24 | }
25 | }
26 | ```
27 |
28 | ## How does it work?
29 |
30 | I've made a video to explain in details the internal implementation of `KeyPathTesting` 👇
31 |
32 | [](https://www.youtube.com/watch?v=58IChtjCPGM)
33 |
34 | ## Requirements
35 |
36 | Xcode 12.5 & Swift 5.4
37 |
38 | ## Installation
39 |
40 | ### SPM
41 |
42 | #### Package.swift
43 |
44 | Add a new dependency to your package by adding the following to `Package.swift`:
45 |
46 | ```
47 | .package(url: "https://github.com/vincent-pradeilles/KeyPathTesting", from: "0.0.1"),
48 | ```
49 |
50 | #### Xcode
51 |
52 | Open your project file in the project navigator and select the project instead of one of your targets. Switch to the Swift Packages tab and click the plus.
53 | When prompted enter the url `https://github.com/vincent-pradeilles/KeyPathTesting`
54 |
55 | ### CocoaPods
56 |
57 | Add the following to your `Podfile`:
58 |
59 | `pod "KeyPathTesting"`
60 |
61 | ### Carthage
62 |
63 | Add the following to your `Cartfile`:
64 |
65 | `github "vincent-pradeilles/KeyPathTesting" == 0.1.4`
66 |
67 | Please note that newer versions of this framework do not officially support Carthage anymore
68 |
69 | ## Author
70 |
71 | * Vincent Pradeilles: [@v_pradeilles](https://twitter.com/v_pradeilles)
72 |
73 | ## Contributors
74 |
75 | * Henrik Panhans: [@henrik_dmg](https://twitter.com/henrik_dmg)
76 |
--------------------------------------------------------------------------------
/Sources/KeyPathTesting/Assert/Assertion.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Assertion.swift
3 | // KeyPathTesting
4 | //
5 | // Created by Vincent on 12/06/2019.
6 | // Copyright © 2019 Vincent. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public typealias RawAssertion = (_ instance: Type, _ file: StaticString, _ line: UInt) -> ()
12 |
13 | public struct Assertion {
14 |
15 | public let assertion: RawAssertion
16 | public let file: StaticString
17 | public let line: UInt
18 |
19 | public init(assertion: @escaping RawAssertion, file: StaticString, line: UInt) {
20 | self.assertion = assertion
21 | self.file = file
22 | self.line = line
23 | }
24 |
25 | public func assert(on instance: Type) {
26 | assertion(instance, file, line)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/KeyPathTesting/Assert/AssertionBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AssertionBuilder.swift
3 | // KeyPathTesting
4 | //
5 | // Created by Vincent on 12/06/2019.
6 | // Copyright © 2019 Vincent. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @resultBuilder
12 | public struct AssertionBuilder {
13 |
14 | public static func buildExpression(
15 | _ expression: @escaping RawAssertion,
16 | _ file: StaticString = #file,
17 | _ line: UInt = #line) -> Assertion
18 | {
19 | Assertion(assertion: expression, file: file, line: line)
20 | }
21 |
22 | public static func buildBlock(_ children: Assertion...) -> [Assertion] {
23 | children
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/KeyPathTesting/Assert/TopLevelFunctions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopLevelFunctions.swift
3 | // KeyPathTesting
4 | //
5 | // Created by Vincent on 12/06/2019.
6 | // Copyright © 2019 Vincent. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public func assert(on instance: Type, @AssertionBuilder assertions: () -> [Assertion]) {
12 | assertions().forEach { $0.assert(on: instance) }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/KeyPathTesting/Operators/Operators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Operators.swift
3 | // KeyPathTesting
4 | //
5 | // Created by Vincent on 12/06/2019.
6 | // Copyright © 2019 Vincent. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | public func == (property: KeyPath, constant: Value) -> RawAssertion {
12 | return { instance, file, line in
13 | XCTAssertEqual(instance[keyPath: property], constant, file: file, line: line)
14 | }
15 | }
16 |
17 | public func != (property: KeyPath, constant: Value) -> RawAssertion {
18 | return { instance, file, line in
19 | XCTAssertNotEqual(instance[keyPath: property], constant, file: file, line: line)
20 | }
21 | }
22 |
23 | public func ~= (_ pattern: ClosedRange, _ property: KeyPath) -> RawAssertion {
24 | return { instance, file, line in
25 | XCTAssert(pattern ~= instance[keyPath: property], file: file, line: line)
26 | }
27 | }
28 |
29 | public func < (property: KeyPath, constant: Value) -> RawAssertion {
30 | return { instance, file, line in
31 | XCTAssertLessThan(instance[keyPath: property], constant, file: file, line: line)
32 | }
33 | }
34 |
35 | public func <= (property: KeyPath, constant: Value) -> RawAssertion {
36 | return { instance, file, line in
37 | XCTAssertLessThanOrEqual(instance[keyPath: property], constant, file: file, line: line)
38 | }
39 | }
40 |
41 | public func > (property: KeyPath, constant: Value) -> RawAssertion {
42 | return { instance, file, line in
43 | XCTAssertGreaterThan(instance[keyPath: property], constant, file: file, line: line)
44 | }
45 | }
46 |
47 | public func >= (property: KeyPath, constant: Value) -> RawAssertion {
48 | return { instance, file, line in
49 | XCTAssertGreaterThanOrEqual(instance[keyPath: property], constant, file: file, line: line)
50 | }
51 | }
52 |
53 | public prefix func ! (property: KeyPath) -> RawAssertion {
54 | return { instance, file, line in
55 | XCTAssertFalse(instance[keyPath: property], file: file, line: line)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Tests/KeyPathTestingTests/OperatorsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OperatorsTests.swift
3 | // KeyPathTestingTests
4 | //
5 | // Created by Vincent PRADEILLES on 12/06/2019.
6 | // Copyright © 2019 Vincent. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import KeyPathTesting
11 |
12 | class OperatorsTests: XCTestCase {
13 |
14 | func testOperators() {
15 | let myData = [1, 2, 3, 4]
16 |
17 | assert(on: myData) {
18 | \.isEmpty == false
19 | \.isEmpty != true
20 | !\.isEmpty
21 | \.count > 2
22 | \.count >= 4
23 | \.count < 7
24 | \.count <= 4
25 | \.first == 1
26 | \.last != 1
27 | }
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------