Skip to content

Commit

Permalink
feat: create mocks for push messaging (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian authored Sep 13, 2021
1 parent fc67e44 commit b2c5d62
Show file tree
Hide file tree
Showing 15 changed files with 736 additions and 24 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ SHELL = /bin/sh
# imports - Import statements to be at the top of the generated files in case the file needs classes from other modules. Split by `:` (example: `imports=Cio-Foo-Bar`)
generate:
sourcery --sources Sources/Tracking --templates Sources/Templates --output Sources/Tracking/autogenerated --args diAccessLevel=public,moduleName=Tracking
sourcery --sources Sources/MessagingPush --templates Sources/Templates --output Sources/MessagingPush/autogenerated --args diAccessLevel=public,moduleName=MessagingPush,imports=CioTracking
sourcery --sources Sources/MessagingPushAPN --templates Sources/Templates --output Sources/MessagingPushAPN/autogenerated --args diAccessLevel=internal,moduleName=MessagingPushAPN,imports=CioMessagingPush:CioTracking

lint:
swiftlint lint --strict
Expand Down
11 changes: 10 additions & 1 deletion Sources/MessagingPush/MessagingPush.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import CioTracking
import Foundation

public protocol MessagingPushInstance: AutoMockable {
func registerDeviceToken(_ deviceToken: Data, onComplete: @escaping (Result<Void, CustomerIOError>) -> Void)
func deleteDeviceToken(onComplete: @escaping (Result<Void, CustomerIOError>) -> Void)
}

/**
Swift code goes into this module that are common to *all* of the Messaging Push modules (APN, FCM, etc).
So, performing an HTTP request to the API with a device token goes here.
*/
open class MessagingPush {

public class MessagingPush: MessagingPushInstance {
@Atomic public private(set) static var shared = MessagingPush(customerIO: CustomerIO.shared)

public let customerIO: CustomerIO!
Expand Down Expand Up @@ -110,3 +116,6 @@ open class MessagingPush {
}
}
}

// sourcery: InjectRegister = "DiPlaceholder"
internal class DiPlaceholder {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable all

import CioTracking
import Foundation

// File generated from Sourcery-DI project: https://github.com/levibostian/Sourcery-DI
// Template version 1.0.0

/**
######################################################
Documentation
######################################################

This automatically generated file you are viewing is a dependency injection graph for your app's source code.
You may be wondering a couple of questions.

1. How did this file get generated? Answer --> https://github.com/levibostian/Sourcery-DI#how
2. Why use this dependency injection graph instead of X other solution/tool? Answer --> https://github.com/levibostian/Sourcery-DI#why-use-this-project
3. How do I add dependencies to this graph file? Follow one of the instructions below:
* Add a non singleton class: https://github.com/levibostian/Sourcery-DI#add-a-non-singleton-class
* Add a generic class: https://github.com/levibostian/Sourcery-DI#add-a-generic-class
* Add a singleton class: https://github.com/levibostian/Sourcery-DI#add-a-singleton-class
* Add a class from a 3rd party library/SDK: https://github.com/levibostian/Sourcery-DI#add-a-class-from-a-3rd-party
* Add a `typealias` https://github.com/levibostian/Sourcery-DI#add-a-typealias

4. How do I get dependencies from the graph in my code?
```
// If you have a class like this:
class OffRoadWheels {}

class ViewController: UIViewController {
// Call the property getter to get your dependency from the graph:
let wheels = DIMessagingPush.shared.offRoadWheels
// note the name of the property is name of the class with the first letter lowercase.

// you can also use this syntax instead:
let wheels: OffRoadWheels = DIMessagingPush.shared.inject(.offRoadWheels)
// although, it's not recommended because `inject()` performs a force-cast which could cause a runtime crash of your app.
}
```

5. How do I use this graph in my test suite?
```
let mockOffRoadWheels = // make a mock of OffRoadWheels class
DIMessagingPush.shared.override(.offRoadWheels, mockOffRoadWheels)
```

Then, when your test function finishes, reset the graph:
```
DIMessagingPush.shared.resetOverrides()
```

*/

/**
enum that contains list of all dependencies in our app.
This allows automated unit testing against our dependency graph + ability to override nodes in graph.
*/
public enum DependencyMessagingPush: CaseIterable {
case diPlaceholder
}

/**
Dependency injection graph specifically with dependencies in the MessagingPush module.

We must use 1+ different graphs because of the hierarchy of modules in this SDK.
Example: You can't add classes from `Tracking` module in `Common`'s DI graph. However, classes
in `Common` module can be in the `Tracking` module.
*/
public class DIMessagingPush {
public static var shared = DIMessagingPush()
private var overrides: [DependencyMessagingPush: Any] = [:]
private init() {}

/**
Designed to be used only in test classes to override dependencies.

```
let mockOffRoadWheels = // make a mock of OffRoadWheels class
DIMessagingPush.shared.override(.offRoadWheels, mockOffRoadWheels)
```
*/
public func override<Value: Any>(_ dep: DependencyMessagingPush, value: Value, forType type: Value.Type) {
overrides[dep] = value
}

/**
Reset overrides. Meant to be used in `tearDown()` of tests.
*/
public func resetOverrides() {
overrides = [:]
}

/**
Use this generic method of getting a dependency, if you wish.
*/
public func inject<T>(_ dep: DependencyMessagingPush) -> T {
switch dep {
case .diPlaceholder: return diPlaceholder as! T
}
}

/**
Use the property accessors below to inject pre-typed dependencies.
*/

// DiPlaceholder
internal var diPlaceholder: DiPlaceholder {
if let overridenDep = overrides[.diPlaceholder] {
return overridenDep as! DiPlaceholder
}
return newDiPlaceholder
}

private var newDiPlaceholder: DiPlaceholder {
DiPlaceholder()
}
}
61 changes: 61 additions & 0 deletions Sources/MessagingPush/autogenerated/AutoLenses.generated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable all

import CioTracking
import Foundation

/**
######################################################
Documentation
######################################################

This automatically generated file you are viewing is to modify immutable objects in a convenient way.

* What do you mean modify immutable objects? https://apiumhub.com/tech-blog-barcelona/lenses-swift-immutability-objects/

* How do I use this?

```
// Add `AutoLenses` protocol to struct.
struct Foo: AutoLenses {
// properties be `let` to be immutable
let bar: String
let bar2: Bool
}

var foo = Foo(bar: "X", bar2: true)
// Now, we want to modify `foo.bar` but it's immutable. How do I modify `bar` without having to do something messy like...
Foo(bar: "new value", bar2: oldInstance.bar2)
...to leave my other properties alone?

foo = foo.setBar("new value")
// Now, `foo` is set to the brand new instanced of `Foo` while copying over all other values of `Foo` from the old instance.
```

*/

infix operator *~: MultiplicationPrecedence
infix operator |>: AdditionPrecedence

struct Lens<Whole, Part> {
let get: (Whole) -> Part
let set: (Part, Whole) -> Whole
}

func * <A, B, C>(lhs: Lens<A, B>, rhs: Lens<B, C>) -> Lens<A, C> {
Lens<A, C>(get: { a in rhs.get(lhs.get(a)) },
set: { c, a in lhs.set(rhs.set(c, lhs.get(a)), a) })
}

func *~ <A, B>(lhs: Lens<A, B>, rhs: B) -> (A) -> A {
{ a in lhs.set(rhs, a) }
}

func |> <A, B>(x: A, f: (A) -> B) -> B {
f(x)
}

func |> <A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
{ g(f($0)) }
}
149 changes: 149 additions & 0 deletions Sources/MessagingPush/autogenerated/AutoMockable.generated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Generated using Sourcery 1.5.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable all

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import CioTracking

/**
######################################################
Documentation
######################################################

This automatically generated file you are viewing contains mock classes that you can use in your test suite.

* How do you generate a new mock class?

1. Mocks are generated from Swift protocols. So, you must make one.

```
protocol FriendsRepository {
func acceptFriendRequest<Attributes: Encodable>(attributes: Attributes, _ onComplete: @escaping () -> Void)
}

class AppFriendsRepository: FriendsRepository {
...
}
```

2. Have your new protocol extend `AutoMockable`:

```
protocol FriendsRepository: AutoMockable {
func acceptFriendRequest<Attributes: Encodable>(
// sourcery:Type=Encodable
attributes: Attributes,
_ onComplete: @escaping () -> Void)
}
```

> Notice the use of `// sourcery:Type=Encodable` for the generic type parameter. Without this, the mock would
fail to compile: `functionNameReceiveArguments = (Attributes)` because `Attributes` is unknown to this `var`.
Instead, we give the parameter a different type to use for the mock. `Encodable` works in this case.
It will require a cast in the test function code, however.

3. Run the command `make generate` on your machine. The new mock should be added to this file.

* How do you use the mocks in your test class?

```
class ExampleViewModelTest: XCTestCase {
var viewModel: ExampleViewModel!
var exampleRepositoryMock: ExampleRepositoryMock!
override func setUp() {
exampleRepositoryMock = ExampleRepositoryMock()
viewModel = AppExampleViewModel(exampleRepository: exampleRepositoryMock)
}
}
```

Or, you may need to inject the mock in a different way using the DI.shared graph:

```
class ExampleTest: XCTestCase {
var exampleViewModelMock: ExampleViewModelMock!
var example: Example!
override func setUp() {
exampleViewModelMock = ExampleViewModelMock()
DI.shared.override(.exampleViewModel, value: exampleViewModelMock, forType: ExampleViewModel.self)
example = Example()
}
}

```

*/

/**
Class to easily create a mocked version of the `MessagingPushInstance` class.
This class is equipped with functions and properties ready for you to mock!

Note: This file is automatically generated. This means the mocks should always be up-to-date and has a consistent API.
See the SDK documentation to learn the basics behind using the mock classes in the SDK.
*/
public class MessagingPushInstanceMock: MessagingPushInstance {
/// If *any* interactions done on mock. `true` if any method or property getter/setter called.
public var mockCalled: Bool = false //

// MARK: - registerDeviceToken

/// Number of times the function was called.
public private(set) var registerDeviceTokenCallsCount = 0
/// `true` if the function was ever called.
public var registerDeviceTokenCalled: Bool {
registerDeviceTokenCallsCount > 0
}

/// The arguments from the *last* time the function was called.
public private(set) var registerDeviceTokenReceivedArguments: (deviceToken: Data,
onComplete: (Result<Void, CustomerIOError>) -> Void)?
/// Arguments from *all* of the times that the function was called.
public private(set) var registerDeviceTokenReceivedInvocations: [(deviceToken: Data,
onComplete: (Result<Void, CustomerIOError>)
-> Void)] = []
/**
Set closure to get called when function gets called. Great way to test logic or return a value for the function.
*/
public var registerDeviceTokenClosure: ((Data, (Result<Void, CustomerIOError>) -> Void) -> Void)?

/// Mocked function for `registerDeviceToken(_ deviceToken: Data, onComplete: @escaping (Result<Void, CustomerIOError>) -> Void)`. Your opportunity to return a mocked value and check result of mock in test code.
public func registerDeviceToken(_ deviceToken: Data,
onComplete: @escaping (Result<Void, CustomerIOError>) -> Void)
{
mockCalled = true
registerDeviceTokenCallsCount += 1
registerDeviceTokenReceivedArguments = (deviceToken: deviceToken, onComplete: onComplete)
registerDeviceTokenReceivedInvocations.append((deviceToken: deviceToken, onComplete: onComplete))
registerDeviceTokenClosure?(deviceToken, onComplete)
}

// MARK: - deleteDeviceToken

/// Number of times the function was called.
public private(set) var deleteDeviceTokenCallsCount = 0
/// `true` if the function was ever called.
public var deleteDeviceTokenCalled: Bool {
deleteDeviceTokenCallsCount > 0
}

/// The arguments from the *last* time the function was called.
public private(set) var deleteDeviceTokenReceivedArguments: ((Result<Void, CustomerIOError>) -> Void)?
/// Arguments from *all* of the times that the function was called.
public private(set) var deleteDeviceTokenReceivedInvocations: [(Result<Void, CustomerIOError>) -> Void] = []
/**
Set closure to get called when function gets called. Great way to test logic or return a value for the function.
*/
public var deleteDeviceTokenClosure: (((Result<Void, CustomerIOError>) -> Void) -> Void)?

/// Mocked function for `deleteDeviceToken(onComplete: @escaping (Result<Void, CustomerIOError>) -> Void)`. Your opportunity to return a mocked value and check result of mock in test code.
public func deleteDeviceToken(onComplete: @escaping (Result<Void, CustomerIOError>) -> Void) {
mockCalled = true
deleteDeviceTokenCallsCount += 1
deleteDeviceTokenReceivedArguments = onComplete
deleteDeviceTokenReceivedInvocations.append(onComplete)
deleteDeviceTokenClosure?(onComplete)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import CioMessagingPush
import CioTracking
import Foundation

public protocol MessagingPushAPNInstance: AutoMockable {
// sourcery:Name=didRegisterForRemoteNotifications
func application(
_ application: Any,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data,
onComplete: @escaping (Result<Void, CustomerIOError>) -> Void
)

// sourcery:Name=didFailToRegisterForRemoteNotifications
func application(
_ application: Any,
didFailToRegisterForRemoteNotificationsWithError error: Error,
onComplete: @escaping (Result<Void, CustomerIOError>) -> Void
)
}

/**
MessagingPush extension to support APN push notification messaging.
*/
Expand All @@ -22,3 +38,6 @@ public extension MessagingPush {
deleteDeviceToken(onComplete: onComplete)
}
}

// sourcery: InjectRegister = "DiPlaceholder"
internal class DiPlaceholder {}
Loading

0 comments on commit b2c5d62

Please sign in to comment.