Skip to content

Commit 5e7ab69

Browse files
authored
Merge pull request #5010 from woocommerce/issue/4384-connectivity-service-unit-tests
Add unit tests for Connectivity Service
2 parents f341fe0 + fcf09d0 commit 5e7ab69

File tree

3 files changed

+179
-9
lines changed

3 files changed

+179
-9
lines changed

WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ final class DefaultConnectivityObserver: ConnectivityObserver {
55

66
/// Network monitor to evaluate connection.
77
///
8-
private let networkMonitor: NWPathMonitor
8+
private let networkMonitor: NetworkMonitoring
99
private let observingQueue: DispatchQueue = .global(qos: .background)
1010

1111
var isConnectivityAvailable: Bool {
12-
if case .reachable = connectivityStatus(from: networkMonitor.currentPath) {
12+
if case .reachable = connectivityStatus(from: networkMonitor.currentNetwork) {
1313
return true
1414
}
1515
return false
1616
}
1717

18-
init(networkMonitor: NWPathMonitor = .init()) {
18+
init(networkMonitor: NetworkMonitoring = NWPathMonitor()) {
1919
self.networkMonitor = networkMonitor
2020
startObserving()
2121
}
@@ -25,7 +25,7 @@ final class DefaultConnectivityObserver: ConnectivityObserver {
2525
}
2626

2727
func updateListener(_ listener: @escaping (ConnectivityStatus) -> Void) {
28-
networkMonitor.pathUpdateHandler = { [weak self] path in
28+
networkMonitor.networkUpdateHandler = { [weak self] path in
2929
guard let self = self else { return }
3030
let connectivityStatus = self.connectivityStatus(from: path)
3131
DispatchQueue.main.async {
@@ -38,7 +38,7 @@ final class DefaultConnectivityObserver: ConnectivityObserver {
3838
networkMonitor.cancel()
3939
}
4040

41-
private func connectivityStatus(from path: NWPath) -> ConnectivityStatus {
41+
private func connectivityStatus(from path: NetworkMonitorable) -> ConnectivityStatus {
4242
let connectivityStatus: ConnectivityStatus
4343
switch path.status {
4444
case .satisfied:
@@ -60,3 +60,51 @@ final class DefaultConnectivityObserver: ConnectivityObserver {
6060
return connectivityStatus
6161
}
6262
}
63+
64+
// MARK: - Testability
65+
66+
/// Proxy protocol for mocking `NWPathMonitor`.
67+
protocol NetworkMonitoring: AnyObject {
68+
var currentNetwork: NetworkMonitorable { get }
69+
70+
/// A handler that receives network updates.
71+
var networkUpdateHandler: ((NetworkMonitorable) -> Void)? { get set }
72+
73+
/// Starts monitoring network changes, and sets a queue on which to deliver events.
74+
func start(queue: DispatchQueue)
75+
76+
/// Stops receiving network monitoring updates.
77+
func cancel()
78+
}
79+
80+
/// Proxy protocol for mocking `NWPath`.
81+
protocol NetworkMonitorable {
82+
/// A status indicating whether a network can be used by connections.
83+
var status: NWPath.Status { get }
84+
85+
/// Checks if the network uses an NWInterface with the specified type
86+
func usesInterfaceType(_ type: NWInterface.InterfaceType) -> Bool
87+
}
88+
89+
extension NWPath: NetworkMonitorable {}
90+
extension NWPathMonitor: NetworkMonitoring {
91+
var currentNetwork: NetworkMonitorable {
92+
currentPath
93+
}
94+
95+
var networkUpdateHandler: ((NetworkMonitorable) -> Void)? {
96+
get {
97+
let closure: ((NetworkMonitorable) -> Void)? = {
98+
[weak self] network in
99+
guard let path = network as? NWPath else {
100+
return
101+
}
102+
self?.pathUpdateHandler?(path)
103+
}
104+
return closure
105+
}
106+
set {
107+
pathUpdateHandler = newValue
108+
}
109+
}
110+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,10 +1297,11 @@
12971297
DE525499268C8B32007A5829 /* UIRefreshControl+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */; };
12981298
DE67D46726B98FD000EFE8DB /* Publisher+WithLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */; };
12991299
DE67D46926BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */; };
1300-
DE792E1826EF35F40071200C /* ConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1726EF35F40071200C /* ConnectivityObserver.swift */; };
1301-
DE792E1B26EF37ED0071200C /* DefaultConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */; };
13021300
DE7842ED26F061650030C792 /* NumberFormatter+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */; };
13031301
DE7842EF26F079A60030C792 /* NumberFormatter+LocalizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */; };
1302+
DE7842F926F435070030C792 /* DefaultConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842F826F435070030C792 /* DefaultConnectivityObserver.swift */; };
1303+
DE792E1826EF35F40071200C /* ConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1726EF35F40071200C /* ConnectivityObserver.swift */; };
1304+
DE792E1B26EF37ED0071200C /* DefaultConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */; };
13041305
DE8C94662646990000C94823 /* PluginListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C94652646990000C94823 /* PluginListViewController.swift */; };
13051306
DE8C946E264699B600C94823 /* PluginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C946D264699B600C94823 /* PluginListViewModel.swift */; };
13061307
DEC2961F26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC2961E26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift */; };
@@ -2736,10 +2737,11 @@
27362737
DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+Woo.swift"; sourceTree = "<group>"; };
27372738
DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFrom.swift"; sourceTree = "<group>"; };
27382739
DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFromTests.swift"; sourceTree = "<group>"; };
2739-
DE792E1726EF35F40071200C /* ConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityObserver.swift; sourceTree = "<group>"; };
2740-
DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConnectivityObserver.swift; sourceTree = "<group>"; };
27412740
DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Localized.swift"; sourceTree = "<group>"; };
27422741
DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+LocalizedTests.swift"; sourceTree = "<group>"; };
2742+
DE7842F826F435070030C792 /* DefaultConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConnectivityObserver.swift; sourceTree = "<group>"; };
2743+
DE792E1726EF35F40071200C /* ConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityObserver.swift; sourceTree = "<group>"; };
2744+
DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConnectivityObserver.swift; sourceTree = "<group>"; };
27432745
DE8C94652646990000C94823 /* PluginListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewController.swift; sourceTree = "<group>"; };
27442746
DE8C946D264699B600C94823 /* PluginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewModel.swift; sourceTree = "<group>"; };
27452747
DEC2961E26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCustomsFormListViewModel.swift; sourceTree = "<group>"; };
@@ -4944,6 +4946,7 @@
49444946
0277AEAA256CAA5300F45C4A /* MockShippingLabelAddress.swift */,
49454947
0211252D25773FB00075AD2A /* MockAggregateOrderItem.swift */,
49464948
4590B651261C8D1E00A6FCE0 /* WeightFormatterTests.swift */,
4949+
DE7842F826F435070030C792 /* DefaultConnectivityObserver.swift */,
49474950
);
49484951
path = Tools;
49494952
sourceTree = "<group>";
@@ -7928,6 +7931,7 @@
79287931
450C2CB324D0803000D570DD /* ProductSettingsRowsTests.swift in Sources */,
79297932
45AF9DAF265CFAB4001EB794 /* MockShippingLabelCarrierRate.swift in Sources */,
79307933
CC593A6726EA116300EF0E04 /* ShippingLabelAddNewPackageViewModelTests.swift in Sources */,
7934+
DE7842F926F435070030C792 /* DefaultConnectivityObserver.swift in Sources */,
79317935
455A2FDB246B1349000CA72C /* ProductVisibilityTests.swift in Sources */,
79327936
0215C6FC2518A3CD005240CD /* ProductFormViewModel+SaveTests.swift in Sources */,
79337937
265284092624ACE900F91BA1 /* AddOnCrossreferenceTests.swift in Sources */,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import Network
2+
import XCTest
3+
@testable import WooCommerce
4+
5+
final class DefaultConnectivityObserverTests: XCTestCase {
6+
func test_initializing_observer_triggers_network_monitoring() {
7+
// Given
8+
let network = MockNetwork(status: .satisfied, currentInterface: .wifi)
9+
let networkMonitor = MockNetworkMonitor(currentNetwork: network)
10+
11+
// When
12+
let _ = DefaultConnectivityObserver(networkMonitor: networkMonitor)
13+
14+
// Then
15+
XCTAssertTrue(networkMonitor.didStartMonitoring)
16+
}
17+
18+
func test_stopping_observer_stops_network_monitoring() {
19+
// Given
20+
let network = MockNetwork(status: .satisfied, currentInterface: .wifi)
21+
let networkMonitor = MockNetworkMonitor(currentNetwork: network)
22+
23+
// When
24+
let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor)
25+
observer.stopObserving()
26+
27+
// Then
28+
XCTAssertTrue(networkMonitor.didStopMonitoring)
29+
}
30+
31+
func test_isConnectivityAvailable_returns_true_when_network_is_satisfied() {
32+
// Given
33+
let network = MockNetwork(status: .satisfied, currentInterface: .wifi)
34+
let networkMonitor = MockNetworkMonitor(currentNetwork: network)
35+
36+
// When
37+
let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor)
38+
39+
// Then
40+
XCTAssertTrue(observer.isConnectivityAvailable)
41+
}
42+
43+
func test_isConnectivityAvailable_returns_false_when_network_is_unsatisfied() {
44+
// Given
45+
let network = MockNetwork(status: .unsatisfied, currentInterface: .wifi)
46+
let networkMonitor = MockNetworkMonitor(currentNetwork: network)
47+
48+
// When
49+
let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor)
50+
51+
// Then
52+
XCTAssertFalse(observer.isConnectivityAvailable)
53+
}
54+
55+
func test_updateListener_returns_correct_status_in_callback_closure() {
56+
// Given
57+
let network = MockNetwork(status: .satisfied, currentInterface: .wifi)
58+
let networkMonitor = MockNetworkMonitor(currentNetwork: network)
59+
let networkUpdate = MockNetwork(status: .satisfied, currentInterface: .cellular)
60+
let statusExpectation = expectation(description: "Status in callback closure")
61+
62+
// When
63+
let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor)
64+
var result: ConnectivityStatus = .unknown
65+
observer.updateListener { status in
66+
result = status
67+
statusExpectation.fulfill()
68+
}
69+
networkMonitor.fakeNetworkUpdate(network: networkUpdate)
70+
71+
// Then
72+
waitForExpectations(timeout: 0.3, handler: nil)
73+
if case .reachable(let type) = result {
74+
XCTAssertEqual(type, .cellular)
75+
} else {
76+
XCTFail("Incorrect result status in callback closure")
77+
}
78+
}
79+
}
80+
81+
final class MockNetworkMonitor: NetworkMonitoring {
82+
let currentNetwork: NetworkMonitorable
83+
84+
var networkUpdateHandler: ((NetworkMonitorable) -> Void)?
85+
86+
private(set) var didStartMonitoring = false
87+
private(set) var didStopMonitoring = false
88+
89+
init(currentNetwork: NetworkMonitorable) {
90+
self.currentNetwork = currentNetwork
91+
}
92+
93+
func fakeNetworkUpdate(network: NetworkMonitorable) {
94+
networkUpdateHandler?(network)
95+
}
96+
97+
func start(queue: DispatchQueue) {
98+
didStartMonitoring = true
99+
}
100+
101+
func cancel() {
102+
didStopMonitoring = true
103+
}
104+
}
105+
106+
struct MockNetwork: NetworkMonitorable {
107+
let status: NWPath.Status
108+
private let currentInterface: NWInterface.InterfaceType
109+
110+
init(status: NWPath.Status, currentInterface: NWInterface.InterfaceType) {
111+
self.status = status
112+
self.currentInterface = currentInterface
113+
}
114+
115+
func usesInterfaceType(_ type: NWInterface.InterfaceType) -> Bool {
116+
type == currentInterface
117+
}
118+
}

0 commit comments

Comments
 (0)