Skip to content

Commit 603974e

Browse files
XCTAssert(isOpenSource)
Co-authored-by: Brandon Williams <[email protected]>
0 parents  commit 603974e

File tree

11 files changed

+410
-0
lines changed

11 files changed

+410
-0
lines changed

.github/CODE_OF_CONDUCT.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Contributor Covenant Code of Conduct
2+
3+
## Our Pledge
4+
5+
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6+
7+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8+
9+
## Our Standards
10+
11+
Examples of behavior that contributes to a positive environment for our community include:
12+
13+
* Demonstrating empathy and kindness toward other people
14+
* Being respectful of differing opinions, viewpoints, and experiences
15+
* Giving and gracefully accepting constructive feedback
16+
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17+
* Focusing on what is best not just for us as individuals, but for the overall community
18+
19+
Examples of unacceptable behavior include:
20+
21+
* The use of sexualized language or imagery, and sexual attention or
22+
advances of any kind
23+
* Trolling, insulting or derogatory comments, and personal or political attacks
24+
* Public or private harassment
25+
* Publishing others' private information, such as a physical or email
26+
address, without their explicit permission
27+
* Other conduct which could reasonably be considered inappropriate in a
28+
professional setting
29+
30+
## Enforcement Responsibilities
31+
32+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33+
34+
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35+
36+
## Scope
37+
38+
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39+
40+
## Enforcement
41+
42+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [email protected]. All complaints will be reviewed and investigated promptly and fairly.
43+
44+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45+
46+
## Enforcement Guidelines
47+
48+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49+
50+
### 1. Correction
51+
52+
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53+
54+
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55+
56+
### 2. Warning
57+
58+
**Community Impact**: A violation through a single incident or series of actions.
59+
60+
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61+
62+
### 3. Temporary Ban
63+
64+
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65+
66+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67+
68+
### 4. Permanent Ban
69+
70+
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71+
72+
**Consequence**: A permanent ban from any sort of public interaction within the community.
73+
74+
## Attribution
75+
76+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77+
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78+
79+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80+
81+
[homepage]: https://www.contributor-covenant.org
82+
83+
For answers to common questions about this code of conduct, see the FAQ at
84+
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- '*'
10+
11+
jobs:
12+
build:
13+
name: MacOS
14+
runs-on: macOS-latest
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Run tests
18+
run: make test
19+
20+
ubuntu:
21+
name: Ubuntu
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v2
25+
- name: Run tests
26+
run: make test

.github/workflows/format.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Format
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
swift_format:
10+
name: swift-format
11+
runs-on: macOS-latest
12+
steps:
13+
- uses: actions/checkout@v2
14+
- name: Install
15+
run: brew install swift-format
16+
- name: Format
17+
run: make format
18+
- uses: stefanzweifel/[email protected]
19+
with:
20+
commit_message: Run swift-format
21+
branch: 'main'
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
PASS = \033[1;7;32m PASS \033[0m
2+
FAIL = \033[1;7;31m FAIL \033[0m
3+
XCT_FAIL = \033[34mXCTFail\033[0m
4+
EXPECTED_STRING = This is expected to fail!
5+
EXPECTED = \033[31m\"$(EXPECTED_STRING)\"\033[0m
6+
7+
test:
8+
@swift test --enable-test-discovery 2>&1 | grep '$(EXPECTED_STRING)' > /dev/null \
9+
&& (echo "$(PASS) $(XCT_FAIL) was called with $(EXPECTED)" && exit) \
10+
|| (echo "$(FAIL) expected $(XCT_FAIL) to be called with $(EXPECTED)" >&2 && exit 1)
11+
12+
test-linux:
13+
@docker run \
14+
--rm \
15+
-v "$(PWD):$(PWD)" \
16+
-w "$(PWD)" \
17+
swift:5.3 \
18+
bash -c "make test"
19+
20+
format:
21+
@swift format \
22+
--ignore-unparsable-files \
23+
--in-place \
24+
--recursive \
25+
.

Package.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// swift-tools-version:5.1
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "xctest-dynamic-overlay",
7+
products: [
8+
.library(name: "XCTestDynamicOverlay", targets: ["XCTestDynamicOverlay"])
9+
],
10+
targets: [
11+
.target(name: "XCTestDynamicOverlay"),
12+
.testTarget(
13+
name: "XCTestDynamicOverlayTests",
14+
dependencies: ["XCTestDynamicOverlay"]
15+
),
16+
]
17+
)

README.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# XCTest Dynamic Overlay
2+
3+
Define XCTest assertion helpers directly in your application and library code.
4+
5+
## Motivation
6+
7+
It is very common to write test support code for libraries and applications. This often comes in the form of little domain-specific functions or helpers that make it easier for users of your code to formulate assertions on behavior.
8+
9+
Currently there are only two options for writing test support code:
10+
11+
* Put it in a test target, but then you can't access it from multiple other test targets. For whatever reason test targets cannot be imported, and so the test support code will only be available in that one single test target.
12+
* Create a dedicated test support module that ships just the test-specific code. Then you can import this module into as many test targets as you want, while never letting the module interact with your regular, production code.
13+
14+
Neither of these options is ideal. In the first case you cannot share your test support, and the second case will lead you to a proliferation of modules. For each feature you potentially need 3 modules: `MyFeature`, `MyFeatureTests` and `MyFeatureTestSupport`. SPM makes managing this quite easy, but it's still a burden.
15+
16+
It would be far better if we could ship the test support code right along side or actual library or application code. After all, they are intimately related. You can even fence off the test support code in `#if DEBUG ... #endif` if you are worried about leaking test code into production.
17+
18+
However, as soon as you add `import XCTest` to a source file in your application or a library it loads, the target becomes unbuildable:
19+
20+
```swift
21+
import XCTest
22+
```
23+
24+
> 🛑 ld: warning: Could not find or use auto-linked library 'XCTestSwiftSupport'
25+
>
26+
> 🛑 ld: warning: Could not find or use auto-linked framework 'XCTest'
27+
28+
This is due to a confluence of problems, including test header search paths, linker issues, and more. XCTest just doesn't seem to be built to be loaded alongside your application or library code.
29+
30+
## Solution
31+
32+
That doesn't mean we can't try! XCTest Dynamic Overlay is a microlibrary that exposes an `XCTFail` function that can be invoked from anywhere. It dynamically loads XCTest functionality at runtime, which means your code will continue to compile just fine.
33+
34+
```swift
35+
import XCTestDynamicOverlay //
36+
```
37+
38+
## Example
39+
40+
A real world example of using this is in our library, the [Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture). That library vends a `TestStore` type whose purpose is to make it easy to write tests for your application's logic. The `TestStore` uses `XCTFail` internally, and so that forces us to move the code to a dedicated test support module. However, due to how SPM works you cannot currently have that module in the same package as the main module, and so we would be forced to extract it to a separate _repo_. By loading `XCTFail` dynamically we can keep the code where it belongs.
41+
42+
As another example, let's say you have an analytics dependency that is used all over your application:
43+
44+
```swift
45+
struct AnalyticsClient {
46+
var track: (Event) -> Void
47+
48+
struct Event: Equatable {
49+
var name: String
50+
var properties: [String: String]
51+
}
52+
}
53+
```
54+
55+
If you are disciplined about injecting dependencies, you probably have a lot of objects that take an analytics client as an argument (or maybe some other fancy form of DI):
56+
57+
```swift
58+
class LoginViewModel: ObservableObject {
59+
...
60+
61+
init(analytics: AnalyticsClient) {
62+
...
63+
}
64+
65+
...
66+
}
67+
```
68+
69+
When testing this view model you will need to provide an analytics client. Typically this means you will construct some kind of "test" analytics client that buffers events into an array, rather than sending live events to a server, so that you can assert on what events were tracked during a test:
70+
71+
```swift
72+
func testLogin() {
73+
var events: [AnalyticsClient.Event] = []
74+
let viewModel = LoginViewModel(
75+
analytics: .test { events.append($0) }
76+
)
77+
78+
...
79+
80+
XCTAssertEqual(events, [.init(name: "Login Success")])
81+
}
82+
```
83+
84+
This works really well, and it's a great way to get test coverage on something that is notoriously difficult to test.
85+
86+
However, some tests may not use analytics at all. It would make the test suite stronger if the tests that don't use the client could prove that it's never used. This would mean when new events are tracked you could be instantly notified of which test cases need to be updated.
87+
88+
One way to do this is to create an instance of the `AnalyticsClient` type that simply performs an `XCTFail` inside the `track` endpoint:
89+
90+
```swift
91+
import XCTest
92+
93+
extension AnalyticsClient {
94+
static let failing = Self(
95+
track: { _ in XCTFail("AnalyticsClient.track is unimplemented.") }
96+
)
97+
}
98+
```
99+
100+
With this you can write a test that proves analytics are never tracked, and even better you don't have to worry about buffering events into an array anymore:
101+
102+
```swift
103+
func testValidation() {
104+
let viewModel = LoginViewModel(
105+
analytics: .failing
106+
)
107+
108+
...
109+
}
110+
```
111+
112+
However, you cannot ship this code with the target that defines `AnalyticsClient`. You either need to extract it out to a test support module (which means `AnalyticsClient` must also be extracted), or the code must be confined to a test target and thus not shareable.
113+
114+
However, with `XCTestDynamicOverlay` we can have our cake and eat it too 😋. We can define both the client type and the failing instance right next to each in application code without needing to extract out needless modules or targets:
115+
116+
```swift
117+
struct AnalyticsClient {
118+
var track: (Event) -> Void
119+
120+
struct Event: Equatable {
121+
var name: String
122+
var properties: [String: String]
123+
}
124+
}
125+
126+
import XCTestDynamicOverlay
127+
128+
extension AnalyticsClient {
129+
static let failing = Self(
130+
track: { _ in XCTFail("AnalyticsClient.track is unimplemented.") }
131+
)
132+
}
133+
```
134+
135+
## License
136+
137+
This library is released under the MIT license. See [LICENSE](LICENSE) for details.

0 commit comments

Comments
 (0)