Unit Test for getting the PassthroughSubject publisher's value - swift

Is there a way to get the value of a PassthroughSubject publisher in a Unit test?
I want to test that a function returns success and to test this one I want to see when the publisher value is .loaded, then is success.
class HomeViewModel: ObservableObject {
var homeState = PassthroughSubject<StatePublisher, Never>()
func load(item: HomeModel) {
self.homeState.send(.loading)
self.dataSource.load(item: item) { result in
switch result {
case .success:
self.homeState.send(.loaded)
case let .failure(error):
self.homeState.send(.error(message: error.localizedDescription))
}
}
}
}
class HomeViewModelTests: XCTestCase {
var sut: ViewModel!
var subscriptions = Set<AnyCancellable>()
override func setUpWithError() throws {
sut = ViewModel()
}
override func tearDownWithError() throws {
sut = nil
subscriptions = []
}
func testUpdateHomeSuccess() {
let expected = StatePublisher.loaded
var result = StatePublisher.loading
sut.load(item: HomeModel.fixture())
sut.homeState
.sink(receiveValue: { state in
result = state
})
.store(in: &subscriptions)
XCTAssert(
result == expected,
"Home expected to be \(expected) but was \(result)"
)
}
}
I tried a test like this, but sink is never called.

Presumably, dataSource.load operates asynchronously. That means you need to use an XCTestExpectation in your test case. Also, because you're using a PassthroughSubject rather than a CurrentValueSubject, you should create your subscription before you start the load, to be sure you can't miss any published values.
let ex = XCTestExpectation()
sut.homeState
.sink {
if case .loaded = $0 {
ex.fulfill()
}
}
.store(in: &subscriptions)
wait(for: [ex], timeout: 10)
// Test fails if it doesn't call `ex.fulfill()` within 10 seconds.
Read Apple's guide to Testing Asynchronous Operations with Expectations.

I changed my PassthroughSubject to CurrentValueSubject as then the state of the value is present also when executing the XCTestCase.
So this test was always failing: (the PassthroughSubject is declared in viewModel class which is tested)
var someValue: PassthroughSubject<Bool, Never> = .init()
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}
But this one is now successful
var someValue: CurrentValueSubject<Bool, Never> = .init(false)
func someTest() throws {
let exp = expectation(description: "Value changed")
viewModel.functionChangingTheValue()
viewModel.someValue.sink { result in
XCTAssertTrue(result)
exp.fulfill()
}
.store(in: &cancellables)
wait(for: [exp], timeout: 0.1)
}

Related

Unit testing view model that depends on a publisher

I implemented a service class with a function that returns a publisher when some data is loaded:
class Service {
let fileURL: URL // Set somewhere else in the program
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
URLSession.shared.dataTaskPublisher(for: fileURL)
.map( { $0.data } )
.decode(type: [MyModelClass].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
This function is used in my view model class like this:
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let service: Service
init(service: Service) {
self.service = service
loadCityData()
}
func loadModels() {
service.loadModels()
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
I find the view model difficult to unit-test because I don't have the publisher returned from the service available directly in my unit test class, but I have the #Published property instead. So I tried to implement a test like this one:
let expectation = expectation(description: "loadModels")
viewModel.$models
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
finishLoading.fulfill()
}, receiveValue: { _ in
})
.store(in: &cancellables) // class-scoped property
viewModel.loadModels()
wait(for: [expectation], timeout: 10)
The problem is that the receiveComplection callback is never called. If I had the publisher available (the one returned from the Service object), the same code applied to the publisher would run successfully and fulfill the expectation. Instead, the complection is not being called but the receiveValue is being called multiple times. Why?
First, instead of passing the entire service to the view model, just pass in the Publisher itself. In the test, you can pass in a synchronous publisher which makes testing much easier.
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
let cancellable = viewModel.$models
.dropFirst(1)
.sink(receiveValue: { output in
XCTAssertEqual(input, output)
})
viewModel.loadModels()
}
}
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let modelLoader: AnyPublisher<[MyModelClass], Error>
init(modelLoader: AnyPublisher<[MyModelClass], Error>) {
self.modelLoader = modelLoader
}
func loadModels() {
modelLoader
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
Notice that there is no need to setup an expectation and wait for it. This makes for a much faster test.
Even simpler would be to just examine the models property directly:
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
viewModel.loadModels()
XCTAssertEqual(viewModel.models, input)
}
}
For all of these though, what exactly do you think you are testing here? There are no transformations and no logic in this code. You aren't testing to ensure the ViewModel calls into the Service, because in order to do this test at all, you have to mock out the Service. So in reality, the only thing you are doing is testing to see if the test itself mocked out the Service correctly. But what's the point in that? Who cares if the test was set up correctly if it doesn't test production code?
You could use a combination of interface and dependency injection to allow testing.
First you define an interface for service:
protocol ServiceInterface {
func loadModels() -> AnyPublisher<[MyModelClass], Error>
}
Next you make Service conform to this new protocol:
class Service: ServiceInterface {
// ...
}
Now you can inject Service into your ViewModel using the interface defined above:
class ViewModel: ObservableObject {
//...
let service: ServiceInterface
init(service: ServiceInterface = Service()) {
self.service = service
loadModels()
}
//...
}
This means you are able to inject any entity conforming to ServiceInterface into the ViewModel, so let's define one in the test target:
struct MockService: ServiceInterface {
let loadModelsResult: Result<[MyModelClass], Error>
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
loadModelsResult.publisher.eraseToAnyPublisher()
}
}
Lastly let's inject MockService into ViewModel for testing purposes:
func testExample() {
let expectedModels = [MyModelClass()]
let subject = ViewModel(service: MockService(loadModelsResult: .success(expectedModels)))
let expectation = expectation(description: "expect models to get loaded")
subject
.$models
.sink(
receiveCompletion: { _ in },
receiveValue: { actualModels in
// or any other test that is meaningful in your context
if actualModels == expectedModels {
expectation.fulfill()
}
}
)
.store(in: &cancellables)
subject.loadModels()
waitForExpectations(timeout: 0.5)
}

how to return Error from Service layer to ViewModel using PassthroughSubject Error handling

I am new to Combine Framework, and facing issue while returning error from Service Layer to ViewModel layer.
Attaching sample code here for reference:
ViewModel
final class ViewModel: ObservableObject {
#Published var user: User?
func sendUserRequest() {
#Injected var userService: UserServiceContract
let publisher = userService.get()
publisher.sink(receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
self?.error = error
}
}, receiveValue: { user in
self.user = user
})
.store(in: cancelBag)
}
}
Service
protocol UserServiceContract {
func get() -> AnyPublisher<User, Error>
}
class UserService: UserServiceContract {
private var cancelBag = CancelBag()
private let subject = PassthroughSubject<User, Error>()
private var requestHoldBackTimeInterval: TimeInterval {
return 0.5
}
func get() -> AnyPublisher<User, Error> {
let repo:AnyPublisher<User, Error> = UserWebRepository().get()
repo.ensureTimeSpan(requestHoldBackTimeInterval)
.mapError({ (error) -> Error in
return error
})
.sink(
receiveCompletion: { _ in },
receiveValue: {
print($0)
self.subject.send($0)
}
)
.store(in: cancelBag)
return subject.eraseToAnyPublisher()
}
}
In Service layer:
a. I am able to pass response to ViewModel from Service when result is successful - using passthroughSubject.
b. I am stuck when result is Error: how to handle it in Service layer and later pass that error to ViewModel?
I am making network call using Combine framework and need help in service layer.
View <> ViewModel <> **Service** <> Repository
You can use .assign(to: &$user) instead of .sink and .store.
The pipeline is usually created in the ObservableObject's init.

Prevent self retain cycle in combine flatMap

I'm having a hard time figuring out how to create a Swift Combine pipeline that contains a .flatMap which has a reference to self. In order to prevent a retain cycle, this should be a [weak self] reference, but this does not work with a .flatMap.
Here is a simplified example showing my problem:
import Foundation
import Combine
class SomeService {
func someOperation(label: String) -> Future<String, Never> {
Future { promise in
print("Starting work for", label)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Finished work for", label)
promise(.success(label))
}
}
}
}
class SomeDataSource {
let someService = SomeService()
var cancellables = Set<AnyCancellable>()
deinit {
print("Deinit SomeDataSource")
}
func complexOperation() {
// First part 'defined' is inside the complexOperation method:
someService.someOperation(label: "First part")
// Second part is 'defined' in another method (it is shared with other tasks)
.flatMap { _ in self.partOfComplexOperation(label: "Second part") } // <--- This creates a retain cycle
.sink { label in
print("Received value in sink", label)
}
.store(in: &cancellables)
}
func partOfComplexOperation(label: String) -> Future<String, Never> {
someService.someOperation(label: label)
}
}
var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
Output:
Starting work for First part
Finished work for First part
Starting work for Second part
Finished work for Second part
Received value in sink Second part
Deinit SomeDataSource
The problem here is that I want my SomeDataSource to be deinitialised right after starting the "First part" and not even starting the second part. So the output I'm looking for is:
Starting work for First part
Deinit SomeDataSource
Finished work for First part
I'm looking for some kind of combination of .flatMap and .compactMap. Does this exist? If I first .compactMap { [weak self] _ in self } I get the expected result, but maybe there is a better way?
import Foundation
import Combine
class SomeService {
func someOperation(label: String) -> Future<String, Never> {
Future { promise in
print("Starting work for", label)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
print("Finished work for", label)
promise(.success(label))
}
}
}
}
class SomeDataSource {
let someService = SomeService()
var cancellables = Set<AnyCancellable>()
deinit {
print("Deinit SomeDataSource")
}
func complexOperation() {
// First part 'defined' is inside the complexOperation method:
someService.someOperation(label: "First part")
.compactMap { [weak self] _ in self }
// Second part is 'defined' in another method (it is shared with other tasks)
.flatMap { dataSource in dataSource.partOfComplexOperation(label: "Second part") }
.sink { label in
print("Received value in sink", label)
}
.store(in: &cancellables)
}
func partOfComplexOperation(label: String) -> Future<String, Never> {
someService.someOperation(label: label)
}
}
var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
Output:
Starting work for First part
Deinit SomeDataSource
Finished work for First part
The solution here is to not retain self. You don't even want self in the flatMap so why retain it...
let label = someService.someOperation(label: "First part")
.flatMap { [someService] _ in
someService.someOperation(label: label)
}
Of course seeing all this work on someService implies that the service is missing some functionality. Seeing that the result of someOperation is being ignored might also be a red flag.
If you were truly in a situation where you had to use self, then the solution would look like this:
let foo = someOperation()
.flatMap { [weak self] in
self?.otherOperation() ?? Empty(completeImmediately: true)
}
Or you might consider something like:
someOperation()
.compactMap { [weak someService] _ in
someService?.otherOperation()
}
.switchToLatest()
Which will cancel otherOperation() if a new event comes through from someOperation().

Executing a task asynchronously using Combine with Swift

I recently started to study the Combine and ran into a certain problem.
First, I will describe what I am doing.
I trying to use Clean Architecture
Here you can see my Repository
protocol Repository {
func test()
}
class MockRepository: Repository {
func test() {
sleep(3)
}
}
Then I created UseCase
class UseCaseBase<TInput, TOutput> {
var task: TOutput? { return nil }
var repository: Repository
init(_ repository: Repository) {
self.repository = repository
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
promise(.success(self.task!))
})
.eraseToAnyPublisher()
}
}
class MockUseCase: UseCaseBase<String, Int> {
override var task: Int? {
repository.test()
return 1
}
}
And then in a init block ContentView I did something like that
init() {
let useCase = MockUseCase(MockRepository())
var cancellables = Set<AnyCancellable>()
useCase.execute(with: "String")
.sink(receiveValue: { value in
print(value)
})
.store(in: &cancellables)
print("Started")
}
At first, I want to get
"Started"
and then after sleep(3)
value "1"
Now I get
"1" and then "Started"
Your sleep(3) call runs on the main thread, which means that it blocks any other operations, including the code that prints the "Started" text.
I won't be rambling about how bad it is to block the main thread, this is well known information, but this is the reason you see the behaviour you asked about.
I don't see any thread switching code in your question, so if you wish to achieve some kind of asynchronicity, then you can either go with Rob's solution of using dispatch(after:), or do the locomotion (the sleep) on another thread:
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return AnyPublisher(Future<TOutput, Never> { promise in
DispatchQueue.global().async {
promise(.success(self.task!))
}
})
.eraseToAnyPublisher()
}
The first thing I'll mention is that you need to hold a reference to cancellables or your publisher will automatically be cancelled when you add async processing to the chain. Move it out of your init method and into a property.
You can also get rid of the sleep and simply chain to a Delay publisher on a queue of your choice. I chose main.
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.delay(for: 3.0, scheduler: DispatchQueue.main)
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
Another option is to get rid of the delay, get rid of the sleep and fulfill your promise asynchronously:
struct SomeThing {
var cancellables = Set<AnyCancellable>()
init() {
MockUseCase(MockRepository())
.execute(with: "String")
.sink(receiveValue: { print($0) } )
.store(in: &cancellables)
print("Started")
}
}
class MockRepository: Repository {
func test() {
// sleep(3)
}
}
func execute(with payload: TInput) -> AnyPublisher<TOutput, Never> {
return Future<TOutput, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
promise(.success(self.task!))
}
}
.eraseToAnyPublisher()
}

Testing view models that rely on an asynchronous data source

I'm building a SwiftUI app that is using the MVVM pattern. The data source for the view models is provided by a custom Publisher for a Realm database. As I'm trying to be good and do a bit of test-driven development, I wrote a test to ensure that the view model responds appropriately to inputs from the SwiftUI front end (specifically in this instance, only querying the Realm once the UI was displayed). The code functions as expected but the test doesn't...
This is almost certainly because I'm not accounting for background processing / thread issues. My normal approach would to set up an expectation but this doesn't help as I need to use the property I'm interested in to create a Publisher but this completes immediately after emitting the initial state and I don't know how to keep it "alive" until the expectation expires. Can anyone point me in the right direction?
View model:
final class PatientListViewModel: ObservableObject, UnidirectionalDataFlowType {
typealias InputType = Input
enum Input {
case onAppear
}
private var cancellables = Set<AnyCancellable>()
private let onAppearSubject = PassthroughSubject<Void, Never>()
// MARK: Output
#Published private(set) var patients: [Patient] = []
// MARK: Private properties
private let realmSubject = PassthroughSubject<Array<Patient>, Never>()
private let realmService: RealmServiceType
// MARK: Initialiser
init(realmService: RealmServiceType) {
self.realmService = realmService
bindInputs()
bindOutputs()
}
// MARK: ViewModel protocol conformance (functional)
func apply(_ input: Input) {
switch input {
case .onAppear:
onAppearSubject.send()
}
}
// MARK: Private methods
private func bindInputs() {
let _ = onAppearSubject
.flatMap { [realmService] _ in realmService.all(Patient.self) }
.share()
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.subscribe(realmSubject)
.store(in: &cancellables)
}
private func bindOutputs() {
let _ = realmSubject
.assign(to: \.patients, on: self)
.store(in: &cancellables)
}
}
Test class: (very bulky due to my debugging code!)
import XCTest
import RealmSwift
import Combine
#testable import AthenaVS
class AthenaVSTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
private var service: RealmServiceType?
override func setUp() {
service = TestRealmService()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
service = nil
cancellables.removeAll()
}
func testPatientListViewModel() {
let viewModel = PatientListViewModel(realmService: service!)
let expectation = self.expectation(description: #function)
var outcome = Array<Patient>()
let _ = viewModel.patients.publisher.collect()
.handleEvents(receiveSubscription: { (subscription) in
print("Receive subscription")
}, receiveOutput: { output in
print("Received output: \(output)")
outcome = output
}, receiveCompletion: { _ in
print("Receive completion")
expectation.fulfill()
}, receiveCancel: {
print("Receive cancel")
expectation.fulfill()
}, receiveRequest: { demand in
print("Receive request: \(demand)")})
.sink { _ in }
.store(in: &cancellables)
viewModel.apply(.onAppear)
waitForExpectations(timeout: 2, handler: nil)
XCTAssertEqual(outcome.count, 4, "ViewModel state should change once triggered")
}
}
EDITED:
My apologies for the lack of clarity. The rest of the code base is as follows:
SwiftUI View
struct ContentView: View {
#ObservedObject var viewModel: PatientListViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.patients) { patient in
Text(patient.name)
}
.onDelete(perform: delete )
}
.navigationBarTitle("Patients")
.navigationBarItems(trailing:
Button(action: { self.viewModel.apply(.onAdd) })
{ Image(systemName: "plus.circle")
.font(.title)
}
)
}
.onAppear(perform: { self.viewModel.apply(.onAppear) })
}
func delete(at offset: IndexSet) {
viewModel.apply(.onDelete(offset))
}
}
Realm Service
protocol RealmServiceType {
func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object
#discardableResult
func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never>
func deletePatient(_ patient: Patient, from realm: Realm)
}
extension RealmServiceType {
func all<Element>(_ type: Element.Type) -> AnyPublisher<Array<Element>, Never> where Element: Object {
all(type, within: try! Realm())
}
func deletePatient(_ patient: Patient) {
deletePatient(patient, from: try! Realm())
}
}
final class TestRealmService: RealmServiceType {
private let patients = [
Patient(name: "Tiddles"), Patient(name: "Fang"), Patient(name: "Phoebe"), Patient(name: "Snowy")
]
init() {
let realm = try! Realm()
guard realm.isEmpty else { return }
try! realm.write {
for p in patients {
realm.add(p)
}
}
}
func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object {
return Publishers.realm(collection: realm.objects(type).sorted(byKeyPath: "name")).eraseToAnyPublisher()
}
func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never> {
let patient = Patient(name: name)
try! realm.write {
realm.add(patient)
}
return Just(patient).eraseToAnyPublisher()
}
func deletePatient(_ patient: Patient, from realm: Realm) {
try! realm.write {
realm.delete(patient)
}
}
}
Custom Publisher (using Realm as a backend)
/ MARK: Custom publisher - produces a stream of Object arrays in response to change notifcations on a given Realm collection
extension Publishers {
struct Realm<Collection: RealmCollection>: Publisher {
typealias Output = Array<Collection.Element>
typealias Failure = Never // TODO: Not true but deal with this later
let collection: Collection
init(collection: Collection) {
self.collection = collection
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = RealmSubscription(subscriber: subscriber, collection: collection)
subscriber.receive(subscription: subscription)
}
}
}
// MARK: Convenience accessor function to the custom publisher
extension Publishers {
static func realm<Collection: RealmCollection>(collection: Collection) -> Publishers.Realm<Collection> {
return Publishers.Realm(collection: collection)
}
}
// MARK: Custom subscription
private final class RealmSubscription<S: Subscriber, Collection: RealmCollection>: Subscription where S.Input == Array<Collection.Element> {
private var subscriber: S?
private let collection: Collection
private var notificationToken: NotificationToken?
init(subscriber: S, collection: Collection) {
self.subscriber = subscriber
self.collection = collection
self.notificationToken = collection.observe { (changes: RealmCollectionChange) in
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
let _ = subscriber.receive(Array(collection.elements))
// case .update(_, let deletions, let insertions, let modifications):
case .update(_, _, _, _):
let _ = subscriber.receive(Array(collection.elements))
case .error(let error):
fatalError("\(error)")
#warning("Impl error handling - do we want to fail or log and recover?")
}
}
}
func request(_ demand: Subscribers.Demand) {
// no impl as RealmSubscriber is effectively just a sink
}
func cancel() {
subscriber = nil
notificationToken = nil
}
}
The issue I'm experiencing is a failure of the test case. I am anticipating the the view model will map an input (.onAppear) from the SwiftUI front end into an array of 'Patients' and assign this array to its patients property. The code works as expected but XCTAssertEqual fails, reporting that the 'patients' property is an empty array after calling 'viewmodel.assign(.onAppear)'. If I put a property observer on 'patients' it does update as expected but the test is not "seeing" the this.