Skip to content

Commit bc6728a

Browse files
authored
Merge pull request #11 from ra1028/feat/testing-tool
feat: Testing support library
2 parents c0ff7d1 + 68758c8 commit bc6728a

38 files changed

+1061
-739
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ jobs:
2929
swift --version
3030
xcodebuild -version
3131
- name: Test for macOS
32-
run: xcodebuild test -scheme Hooks -destination "platform=macOS"
32+
run: xcodebuild test -scheme Hooks-Package -destination "platform=macOS"
3333
- name: Test for iOS
34-
run: xcodebuild test -scheme Hooks -destination "platform=iOS Simulator,name=iPhone 12 Pro"
34+
run: xcodebuild test -scheme Hooks-Package -destination "platform=iOS Simulator,name=iPhone 12 Pro"
3535
- name: Test for tvOS
36-
run: xcodebuild test -scheme Hooks -destination "platform=tvOS Simulator,name=Apple TV"
36+
run: xcodebuild test -scheme Hooks-Package -destination "platform=tvOS Simulator,name=Apple TV"
3737
- name: Build for watchOS
38-
run: WATCHOS=true xcodebuild build -scheme Hooks -destination "platform=watchOS Simulator,name=Apple Watch Series 6 - 44mm"
38+
run: WATCHOS=true xcodebuild build -scheme Hooks-Package -destination "platform=watchOS Simulator,name=Apple Watch Series 6 - 44mm"
3939

4040
build-examples:
4141
name: Build examples

Package.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@ let package = Package(
1313
],
1414
products: [
1515
.library(name: "Hooks", targets: ["Hooks"]),
16+
.library(name: "HooksTesting", targets: ["HooksTesting"]),
1617
],
1718
targets: [
18-
.target(name: "Hooks"),
19+
.target(
20+
name: "Hooks"
21+
),
22+
.target(
23+
name: "HooksTesting",
24+
dependencies: ["Hooks"]
25+
),
1926
.testTarget(
2027
name: "HooksTests",
21-
dependencies: ["Hooks"]
28+
dependencies: ["HooksTesting"]
29+
),
30+
.testTarget(
31+
name: "HooksTestingTests",
32+
dependencies: ["HooksTesting"]
2233
),
2334
],
2435
swiftLanguageVersions: [.v5]

README.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -403,24 +403,39 @@ See also: [Building Your Own React Hooks](https://reactjs.org/docs/hooks-custom.
403403
## How to Test Your Custom Hooks
404404

405405
So far, we have explained that hooks should be called within `HookScope` or `HookView`. Then, how can the custom hook you have created be tested?
406-
For such purposes, there is an API to create a temporary hook scope independent of SwiftUI view.
406+
To making unit testing of your custom hooks easy, SwiftUI-Hooks provides a simple and complete test utility library `HooksTesting`.
407407

408-
Within the `withTemporaryHookScope` function, you can launch the hook scope multiple times to test state transitions in cases such as when the SwiftUI view is evaluated multiple times.
408+
`HookTester` enables unit testing independent of UI of custom hooks by simulating the behavior on the view of a given hook and managing the result values.
409409

410-
For example:
410+
Example:
411411

412412
```swift
413-
withTemporaryHookScope { scope in
414-
scope {
415-
let count = useState(0)
416-
count.wrappedValue = 1
417-
}
413+
// Your custom hook.
414+
func useCounter() -> (count: Int, increment: () -> Void) {
415+
let count = useState(0)
418416

419-
scope {
420-
let count = useState(0)
421-
XCTAssertEqual(count.wrappedValue, 1) // The previous state is preserved.
417+
let increment = {
418+
count.wrappedValue += 1
422419
}
420+
421+
return (count: count.wrappedValue, increment: increment)
422+
}
423+
```
424+
425+
```swift
426+
let tester = HookTester {
427+
useCounter()
423428
}
429+
430+
XCTAssertEqual(tester.value.count, 0)
431+
432+
tester.value.increment()
433+
434+
XCTAssertEqual(tester.value.count, 1)
435+
436+
tester.update() // Simulates view's update.
437+
438+
XCTAssertEqual(tester.value.count, 1)
424439
```
425440

426441
---

Sources/Hooks/Hook/UseContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public func useContext<T>(_ context: Context<T>.Type) -> T {
1111
useHook(ContextHook(context: context))
1212
}
1313

14-
internal struct ContextHook<T>: Hook {
14+
private struct ContextHook<T>: Hook {
1515
let context: Context<T>.Type
1616
let computation = HookComputation.once
1717

Sources/Hooks/Hook/UseEffect.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,33 @@ public func useEffect(
2727
)
2828
}
2929

30-
internal struct EffectHook: Hook {
30+
/// A hook to use a side effect function that is called the number of times according to the strategy specified by `computation`.
31+
/// Optionally the function can be cancelled when this hook is unmount from the view tree or when the side-effect function is called again.
32+
/// The signature is identical to `useEffect`, but this fires synchronously when the hook is called.
33+
///
34+
/// useLayoutEffect(.always) {
35+
/// print("View is being evaluated")
36+
/// return nil
37+
/// }
38+
///
39+
/// - Parameters:
40+
/// - computation: A computation strategy that to determine when to call the effect function again.
41+
/// - effect: A closure that typically represents a side-effect.
42+
/// It is able to return a closure that to do something when this hook is unmount from the view or when the side-effect function is called again.
43+
public func useLayoutEffect(
44+
_ computation: HookComputation,
45+
_ effect: @escaping () -> (() -> Void)?
46+
) {
47+
useHook(
48+
EffectHook(
49+
computation: computation,
50+
shouldDeferredCompute: false,
51+
effect: effect
52+
)
53+
)
54+
}
55+
56+
private struct EffectHook: Hook {
3157
let computation: HookComputation
3258
let shouldDeferredCompute: Bool
3359
let effect: () -> (() -> Void)?
@@ -45,7 +71,7 @@ internal struct EffectHook: Hook {
4571
}
4672
}
4773

48-
internal extension EffectHook {
74+
private extension EffectHook {
4975
final class State {
5076
var cleanup: (() -> Void)? {
5177
didSet { oldValue?() }

Sources/Hooks/Hook/UseEnvironment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public func useEnvironment<Value>(_ keyPath: KeyPath<EnvironmentValues, Value>)
1010
useHook(EnvironmentHook(keyPath: keyPath))
1111
}
1212

13-
internal struct EnvironmentHook<Value>: Hook {
13+
private struct EnvironmentHook<Value>: Hook {
1414
let keyPath: KeyPath<EnvironmentValues, Value>
1515
let computation = HookComputation.once
1616

Sources/Hooks/Hook/UseLayoutEffect.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.

Sources/Hooks/Hook/UseMemo.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public func useMemo<Value>(
1515
useHook(MemoHook(computation: computation, makeValue: makeValue))
1616
}
1717

18-
internal struct MemoHook<Value>: Hook {
18+
private struct MemoHook<Value>: Hook {
1919
let computation: HookComputation
2020
let makeValue: () -> Value
2121

@@ -32,7 +32,7 @@ internal struct MemoHook<Value>: Hook {
3232
}
3333
}
3434

35-
internal extension MemoHook {
35+
private extension MemoHook {
3636
final class State {
3737
var value: Value?
3838
}

Sources/Hooks/Hook/UsePublisher.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public func usePublisher<P: Publisher>(
2525
)
2626
}
2727

28-
internal struct PublisherHook<P: Publisher>: Hook {
28+
private struct PublisherHook<P: Publisher>: Hook {
2929
let computation: HookComputation
3030
let shouldDeferredCompute = true
3131
let makePublisher: () -> P
@@ -40,12 +40,6 @@ internal struct PublisherHook<P: Publisher>: Hook {
4040

4141
func compute(coordinator: Coordinator) {
4242
coordinator.state.cancellable = makePublisher()
43-
.handleEvents(
44-
receiveSubscription: { _ in
45-
coordinator.state.phase = .running
46-
coordinator.updateView()
47-
}
48-
)
4943
.sink(
5044
receiveCompletion: { completion in
5145
switch completion {
@@ -69,9 +63,9 @@ internal struct PublisherHook<P: Publisher>: Hook {
6963
}
7064
}
7165

72-
internal extension PublisherHook {
66+
private extension PublisherHook {
7367
final class State {
74-
var phase = AsyncPhase<P.Output, P.Failure>.pending
68+
var phase = AsyncPhase<P.Output, P.Failure>.running
7569
var cancellable: AnyCancellable?
7670
}
7771
}

Sources/Hooks/Hook/UsePublisherSubscribe.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public func usePublisherSubscribe<P: Publisher>(
1919
useHook(PublisherSubscribeHook(makePublisher: makePublisher))
2020
}
2121

22-
internal struct PublisherSubscribeHook<P: Publisher>: Hook {
22+
private struct PublisherSubscribeHook<P: Publisher>: Hook {
2323
let makePublisher: () -> P
2424
let computation = HookComputation.once
2525

@@ -73,7 +73,7 @@ internal struct PublisherSubscribeHook<P: Publisher>: Hook {
7373
}
7474
}
7575

76-
internal extension PublisherSubscribeHook {
76+
private extension PublisherSubscribeHook {
7777
final class State {
7878
var phase = AsyncPhase<P.Output, P.Failure>.pending
7979
var isDisposed = false

0 commit comments

Comments
 (0)