Skip to content

Commit

Permalink
feat: allow customizing XCTestExpectation behaviour for ordered ana…
Browse files Browse the repository at this point in the history
…lytics expectations
  • Loading branch information
soumyamahunt committed Oct 12, 2022
1 parent 5cf0ae3 commit d8064b2
Show file tree
Hide file tree
Showing 16 changed files with 725 additions and 525 deletions.
30 changes: 30 additions & 0 deletions Sources/Analytics/AnalyticsHandler/AnyAnalyticsEventHandler.swift
@@ -0,0 +1,30 @@
import struct Foundation.Date

/// A type representing type-erased `AnalyticsHandler`s.
protocol AnyAnalyticsEventHandler: AnalyticsHandler {
/// Type-erased event value type.
typealias AnyEvent = AnyAnalyticsEvent<EventName>
/// Action to perform when analytics event and metadata tracking requested.
var track: (AnyEvent, Date, AnyEvent.Metadata) -> Void { get }
}

extension AnyAnalyticsEventHandler {
/// Propagates event track request to type-erased ``AnalyticsHandler``
/// wrapped during initialization, after erasing type of provided event and metadata.
///
/// - Parameters:
/// - event: The event to track.
/// - time: The time at which event fired.
/// - data: The associated metadata with event.
public func track<Event: AnalyticsEvent>(
event: Event,
at time: Date,
data: Event.Metadata
) where EventName == Event.Name {
track(
event as? AnyEvent ?? AnyEvent(from: event),
time,
data as? AnyEvent.Metadata ?? AnyEvent.Metadata(with: data)
)
}
}
25 changes: 2 additions & 23 deletions Sources/Analytics/AnalyticsHandler/AnyAnalyticsHandler.swift
Expand Up @@ -9,14 +9,12 @@ import struct Foundation.Date
/// and ``AnyMetadata`` respectively and then dispatched to wrapped ``AnalyticsHandler``.
/// As a result of this type-change any type based assertions done at the underlying ``AnalyticsHandler``
/// will be impacted.
public struct AnyAnalyticsHandler<EventName>: AnalyticsHandler {
/// Type-erased event value type.
private typealias AnyEvent = AnyAnalyticsEvent<EventName>
public struct AnyAnalyticsHandler<EventName>: AnyAnalyticsEventHandler {
/// Action to perform when analytics event and metadata tracking requested.
///
/// Sends the event and metadata to ``AnalyticsHandler`` value initialized with
/// erasing their types.
private let track: (AnyEvent, Date, AnyEvent.Metadata) -> Void
internal let track: (AnyEvent, Date, AnyEvent.Metadata) -> Void

/// Creates a type-erased ``AnalyticsHandler`` value that wraps the given instance.
///
Expand All @@ -28,23 +26,4 @@ public struct AnyAnalyticsHandler<EventName>: AnalyticsHandler {
handler.track(event: event, at: time, data: data)
}
}

/// Propagates event track request to type-erased ``AnalyticsHandler``
/// wrapped during initialization, after erasing type of provided event and metadata.
///
/// - Parameters:
/// - event: The event to track.
/// - time: The time at which event fired.
/// - data: The associated metadata with event.
public func track<Event: AnalyticsEvent>(
event: Event,
at time: Date,
data: Event.Metadata
) where EventName == Event.Name {
track(
event as? AnyEvent ?? AnyEvent(from: event),
time,
data as? AnyEvent.Metadata ?? AnyEvent.Metadata(with: data)
)
}
}
Expand Up @@ -8,18 +8,17 @@ import struct Foundation.Date
/// - Important: All the events and metadata tracked by this handler are type-erased to ``AnyAnalyticsEvent``
/// and ``AnyMetadata`` respectively and then dispatched to wrapped ``AnalyticsHandler``.
/// As a result of this type-change any type based assertions done at the underlying ``AnalyticsHandler``
/// will be impacted.
public struct AnyHashableAnalyticsHandler<EventName>: AnalyticsHandler, Hashable
/// will be impacted.
public struct AnyHashableAnalyticsHandler<EventName>: AnyAnalyticsEventHandler,
Hashable
{
/// Type-erased event value type.
private typealias AnyEvent = AnyAnalyticsEvent<EventName>
/// The value wrapped by this instance.
private let handler: AnyHashable
/// Action to perform when analytics event and metadata tracking requested.
///
/// Sends the event and metadata to ``AnalyticsHandler`` value initialized with
/// erasing their types.
private let track: (AnyEvent, Date, AnyEvent.Metadata) -> Void
internal let track: (AnyEvent, Date, AnyEvent.Metadata) -> Void

/// Creates a type-erased ``AnalyticsHandler`` value that wraps the given instance.
///
Expand All @@ -33,25 +32,6 @@ public struct AnyHashableAnalyticsHandler<EventName>: AnalyticsHandler, Hashable
}
}

/// Propagates event track request to type-erased ``AnalyticsHandler``
/// wrapped during initialization, after erasing type of provided event and metadata.
///
/// - Parameters:
/// - event: The event to track.
/// - time: The time at which event fired.
/// - data: The associated metadata with event.
public func track<Event: AnalyticsEvent>(
event: Event,
at time: Date,
data: Event.Metadata
) where EventName == Event.Name {
track(
event as? AnyEvent ?? AnyEvent(from: event),
time,
data as? AnyEvent.Metadata ?? AnyEvent.Metadata(with: data)
)
}

/// Returns a Boolean value indicating whether two type-erased
/// ``AnalyticsHandler`` instances wrap the same value.
///
Expand Down
124 changes: 124 additions & 0 deletions Sources/AnalyticsMock/AnalyticsExpectation.swift
@@ -0,0 +1,124 @@
import XCTest

/// An expectation type that keeps the count of
/// ``fulfill()`` method invocation.
///
/// Instead of using this type directly use following
/// `XCTestCase` convenience methods:
///
/// - `expect(event:on:file:function:line:evaluate:)`
/// - `expect(event:on:file:function:line:)`
///
/// - Important: Do not invoke `fulfill()` method
/// directly on the expectation value wrapped.
public class AnalyticsExpectation {
/// Represents current fulfillment state of
/// ``AnalyticsExpectation``.
///
/// - If expectation is met, state is represented
/// by ``fulfilled``.
/// - If expectation isn't met, state is represented
/// by ``unfulfilled``.
/// - If ``AnalyticsExpectation/fulfill()``
/// invocation exceeds ``AnalyticsExpectation/expectedFulfillmentCount``,
/// state is represented by ``overfulfilled``.
public enum FulfillmentState {
/// Expectation hasn't been met.
case unfulfilled
/// Expectation hast been met.
case fulfilled
/// Expectation has been met but
/// the ``AnalyticsExpectation/fulfill()``
/// invocation exceeds ``AnalyticsExpectation/expectedFulfillmentCount``.
case overfulfilled
}

/// The `XCTestExpectation` value
/// wrapped by this instance.
private let base: XCTestExpectation

/// The number of times ``fulfill()`` must be called
/// before the expectation is completely fulfilled.
///
/// The value of `expectedFulfillmentCount`
/// must be greater than `0`. By default, expectations
/// have an `expectedFulfillmentCount` of `1`.
///
/// - Note: The value of `expectedFulfillmentCount`
/// is ignored when ``isInverted`` is `true`.
public var expectedFulfillmentCount: Int {
get { base.expectedFulfillmentCount }
set { base.expectedFulfillmentCount = newValue }
}

/// Indicates that an assertion should be triggered
/// during testing if the expectation is over-fulfilled.
///
/// When `true`, a call to ``fulfill()`` made
/// after the expectation has already been fulfilled
/// (exceeding ``expectedFulfillmentCount``)
/// will trigger an assertion.
///
/// When `false`, a call to ``fulfill()``
/// after the expectation has already been fulfilled
/// will have no effect.
public var assertForOverFulfill: Bool {
get { base.assertForOverFulfill }
set { base.assertForOverFulfill = newValue }
}

/// Indicates that the expectation is not intended to happen.
///
/// To check that a situation *does not occur* during testing,
/// create an expectation that is fulfilled when the unexpected
/// situation occurs, and set its `isInverted` property to true.
/// Your test will fail immediately if the inverted expectation is fulfilled.
public var isInverted: Bool {
get { base.isInverted }
set { base.isInverted = newValue }
}

/// Indicates the number of times ``fulfill()`` has been called.
///
/// By default, this is set to `0` and increased each time ``fulfill()``
/// invoked. Expectation is completely fulfilled if `currentFulfillmentCount`
/// meets or exceeds ``expectedFulfillmentCount``.
public private(set) var currentFulfillmentCount: Int = 0

/// Represents current fulfillment state of expectation.
///
/// Compares ``currentFulfillmentCount``
/// with ``expectedFulfillmentCount`` and
/// calculates ``FulfillmentState``.
public var state: FulfillmentState {
switch currentFulfillmentCount - expectedFulfillmentCount {
case 0: return .fulfilled
case ..<0: return .unfulfilled
default: return .overfulfilled
}
}

/// Creates an analytics expectation that wraps the given instance.
///
/// Wraps the provided expectation while exposing methods to
/// customize its properties and a fulfillment count.
///
/// - Parameter base: An expectation value to wrap.
/// - Returns: Newly created analytics expectation.
///
/// - Important: Do not invoke `fulfill()` method
/// directly on the expectation provided.
public init(from base: XCTestExpectation) {
self.base = base
}

/// Marks the expectation as having been met.
///
/// It is an error to call this method on an expectation
/// that has already been fulfilled, or when the test case
/// that vended the expectation has already completed.
public func fulfill() {
currentFulfillmentCount += 1
base.fulfill()
}
}

0 comments on commit d8064b2

Please sign in to comment.