As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM
Related
I am new to RxSwift and RxCocoa
I need to any advice for learning
After result of Checking Id Validation, expect no word in label
But it is updating label and no entering in break point at bind function
What’s problem my code…?
var disposeBag: DisposeBag = DisposeBag()
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input: Signal<String> = userIDTextField.rx.text.orEmpty
.asSignal(onErrorSignalWith: .empty())
let output: Driver<String> = viewModel.bind(input)
disposeBag.insert(
output.drive(userIDLabel.rx.text)
)
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Signal<Bool> {
return .just(false).asSignal()
}
func bind(_ input: Signal<String>) -> Driver<String> {
let validState = input
.map { _ in self.checkUserIDFromDB(id:)}
.withLatestFrom(input)
return validState.asDriver(onErrorDriveWith: .empty())
}
}
This line: .map { _ in self.checkUserIDFromDB(id:)} produces a Signal<(String) -> Signal<Bool>> which is likely not what you wanted.
I'm going to assume that the goal here is to pass the entered string to the network request and wait for it to emit. If it emits true then emit the string to the label, otherwise do nothing...
Further, let's simplify things by using the Observable type instead of Signals and Drivers:
final class ViewController: UIViewController {
let userIDTextField = UITextField()
let userIDLabel = UILabel()
let disposeBag = DisposeBag() // this should be a `let` not a `var`
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input = userIDTextField.rx.text.orEmpty
let output = viewModel.bind(input.asObservable())
disposeBag.insert(
output.bind(to: userIDLabel.rx.text)
)
}
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Observable<Bool> { .just(false) }
func bind(_ input: Observable<String>) -> Observable<String> {
input.flatMapLatest { id in // Note this should be `flatMapLatest` not `map`
Observable.zip( // zip up the text with its response
Observable.just(id),
self.checkUserIDFromDB(id: id) // you weren't actually making the network call. This makes it.
.catchAndReturn(false) // if the call fails, emit `false`.
)
}
.compactMap { $0.1 ? $0.0 : nil } // if the response is true, emit the text, else nothing
}
}
The biggest concern I have with this code is what happens if the user continues to type. This will fire after every character the user enters which could be a lot of network requests, the flatMapLatest will cancel ongoing requests that are no longer needed, but still... Consider putting a debounce in the stream to reduce the number of requests.
Learn more about the various versions of flatMap from this article.
Edit
In response to your comment. In my opinion, a ViewModel should not be dependent on RxCocoa, only RxSwift. However, if you feel you must use Driver, then something like this would be appropriate:
func bind(_ input: ControlProperty<String>) -> Driver<String> {
input.asDriver()
.flatMapLatest { id in
Driver.zip(
Driver.just(id),
self.checkUserIDFromDB(id: id)
.asDriver(onErrorJustReturn: false)
)
}
.compactMap { $0.1 ? $0.0 : nil }
}
Using Signal doesn't make much sense in this context.
During the development of a swiftUI project, I wish to have option to switch the way data is set, either
Allowing API calls to be made as normal which sets an #Published variable, or
Set the #published variable from mocked file and NOT make the api call.
The reason is that I am limited to the number of api calls per minute.
In my example below I load the mocked data in a model called "Person".
Current solution
Set a global variable to distinguish between the two above mentioned states.
In all places where api calls were be made, I introduce a condition to optionally use mocked data and not make the api call. See .task in MyView
struct GlobalConstants {
static let use_mock_data = true
}
class ViewModel: ObservableObject {
#Published var data: [Person] = []
#MainActor
func fetchData() async {
// ... data is set in this code
}
}
Within Person model, I set a static variable that returns the decoded mock data from a json file. The decode method below is an extension to Bundle (Thanks Paul Hudsen).
extension Person {
static var mockPersons: [Person] {
Bundle.main.decode([Person].self, from: "persons.json")
}
}
struct MyView: View {
#StateObject var vm = PersonViewModel()
var body: some View {
NavigationView {
List {
ForEach(vm.data) { d in
NavigationLink {
OtherView(prop: d.detail)
} label: {
Text(d.name)
}
}
}
.task { // -----condition--------------------- //
if GlobalConstants.use_mock_data {
vm.data = Person.mockPersons
} else {
await vm.fetchData()
}
}
}
}
}
Question
What other approaches can I consider for enabling the two states? Overriding the methods in some way?
I am still on the learning curve to swift and wondering if theres a better way to enable long term maintenance in a clean and predictable way.
As fetching the data belongs to the view model my suggestion is to put the condition into fetchData.
Marking a method as async doesn't require necessarily that the executed code is asynchronous
#MainActor
class ViewModel: ObservableObject {
#Published var people: [Person] = []
func fetchData() async {
if GlobalConstants.use_mock_data {
people = Person.mockPersons
} else {
// people = await callTheAPI()
}
}
}
and replace the task in the view with
.task {
await vm.fetchData()
}
Note: people is a more meaningful name than data 😃
You should use a Service with a protocol, and then create 2 services extending the protocol, one for mock and one for data, here are some examples from one of my apps (using alamofire, but you can modify it to use it with URLSession or anything else) :
import Alamofire
class NetworkManager {
func get<T: Decodable>(url: URLConvertible, parameters: Parameters?,_ completion: #escaping (DataResponse<T, AFError>) -> Void) {
AF.request(url, parameters: parameters)
.responseDecodable(of: T.self) { response in
completion(response)
}
}
}
protocol SearchServicing {
var parameters: Parameters? { get set }
func get(_ completion: #escaping (Result<SearchResponse, AFError>) -> Void)
}
class SearchService: NetworkManager, SearchServicing {
var parameters: Parameters?
func get(_ completion: #escaping (Result<SearchResponse, AFError>) -> Void) {
let url = "YOUR_URL"
get(url: url, parameters: parameters) { response in
completion(response.result)
}
}
}
class SearchMockService: SearchServicing {
var parameters: Parameters?
var getCallCounter = 0
func get(_ completion: #escaping (Result<SearchResponse, AFError>) -> Void) {
getCallCounter += 1
let response = SearchResponse(recipes: [
Recipe.mock,
Recipe.mock,
Recipe.mock,
])
completion(.success(response))
}
}
Then add it in the ViewModel :
#MainActor
class RecipeViewModel: ObservableObject {
...
private var service: SearchServicing
init(service: SearchServicing) {
self.service = service
}
public func fetchData() {
[...]
service.parameters = ["q": ingredients.map { $0.name }.joined(separator: " ")]
service.get { [weak self] result in
switch result {
case .success(let searchResponse):
self?.totalResults = searchResponse.count
self?.results = searchResponse.recipes
case .failure(let error):
dump(error)
self?.error = AppError(error: error)
}
}
}
}
And so it allows me to do :
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(recipeViewModel: RecipeViewModel(service: SearchMockService()))
}
}
whenever I need to test my view model and
struct MainView: View {
#ObservedResults(RecipeEntity.self) var favorites
#StateObject var recipeViewModel = RecipeViewModel(service: SearchService())
var body: some View {
...
}
}
for non mock datas service
For more informations checkout this video, it helped me to use this structure : How to implement a Mock in Swift!
I'm trying to write some UnitTests for the first time. My pattern is MVP and I'm trying to test my Presenter. I've created mock class: class TeamViewMock: TeamViewPresenterProtocol { }. It contains all the methods from my real Presenter. Inside the each method I'm trying to set the new value for the property, so when the method called - property should get a new value.
Only one property gets new value out of 4 and I've no clue why the other ones didn't get it.
You may see it in the following code
import XCTest
#testable import NHL
class TeamViewPresenterTest: XCTestCase {
var presenter: TeamViewPresenter!
var viewMock: TeamViewMock!
func setupPresenter() {
viewMock = TeamViewMock()
presenter = TeamViewPresenter(with: viewMock)
}
func testGetData() {
setupPresenter()
presenter.getData(completion: {_ in })
XCTAssertTrue(viewMock.isStart) // This one works and returns true
XCTAssertTrue(viewMock.isStop) // Return error
XCTAssertTrue(viewMock.isEndRefreshing) // Return error
XCTAssertTrue(viewMock.isReload) // Return error
}
}
class TeamViewMock: TeamViewPresenterProtocol {
var isStart = false
var isStop = false
var isEndRefreshing = false
var isReload = false
func startAnimating() {
self.isStart = true // Testing stops here and doesn't go any further...
}
func stopAnimating() {
self.isStop = true
}
func endRefreshing() {
self.isEndRefreshing = true
}
func reloadView(_ teams: NHLDTO) {
self.isReload = true
}
}
class TeamViewPresenter {
// MARK: - Public Properties
private weak var view: TeamViewPresenterProtocol?
public let dataFetcherService = DataFetcherService()
// MARK: - Initializers
init(with view: TeamViewPresenterProtocol) {
self.view = view
}
// MARK: - Public Methods
public func getData(completion: #escaping (AppError) -> Void) {
view?.startAnimating() // Testing stops here and doesn't go any further, but still returns true for the property isStart and error for the rest
dataFetcherService.fetchTeamData { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(error)
print(error)
case .success(let teams):
guard let teams = teams else { return }
self.view?.reloadView(teams)
self.view?.stopAnimating()
self.view?.endRefreshing()
}
}
}
}
protocol TeamViewPresenterProtocol: AnyObject {
func startAnimating()
func stopAnimating()
func reloadView(_ teams: NHLDTO)
func endRefreshing()
}
I am chaining some functions together and I can't figure out how to call a completion handler with a return value once all the functions are done running.
class AirQualityProvider {
var aBlock: ((Int?) -> Void)?
func getAirQuality(completion: #escaping (Int?) -> Void) {
aBlock = completion
callAPI()
}
private func callAPI() {
let data = Data()
parseDataForAQI(data: data)
}
private func parseDataForAQI(data: Data) {
for d in data {
dosomeMath(d)
}
}
private func dosomeMath(data: Int) {
// HERE IS WHERE I WANT IT TO SUM UP ALL THE NUMBERS
THEN ONLY RETURN ONE VALUE using a completion handler.
Currently, it returns the average as it is being generated.
}
Almost got it working with help to Alexander. The code Alexander supplied works perfectly, it is amazing. The issue is, when I run taskrunner inside alamofire it returns empty. Outside alamofire it works as usual. I need to run this inside alamofire.
func A(json : JSON){
for (key,subJson) in json{
if subJson["free"].doubleValue > 0.0 {
func B(asset: subJson["asset"].stringValue, json: subJson)
}
}
print(taskRunner.getResults())
}
func B(asset : String, json : JSON){
//OUTSIDE ALAMOFIRE WORKS
self.taskRunner.execute{
return 100
}
Alamofire.request(url).responseJSON { response in
//INSIDE ALAMOFIRE DOESN'T WORK. Returns []
self.taskRunner.execute{
return 100
}
}
}
I would use a dispatch queue to synchronize the aggregation of results (by synchronizing Array.append(_:) calls, and the subsequent reading of the array). Here's a simple example:
import Dispatch
import Foundation
class ParallelTaskRunner<Result> {
private var results = [Result]()
private let group = DispatchGroup()
private let resultAggregatorQueue = DispatchQueue(label: "Result Aggregator")
func execute(_ closure: (#escaping (Result) -> Void) -> Void) {
group.enter() // Register that a new task is in-flight
closure { result in
self.resultAggregatorQueue.sync { // Synchronize access to the array
self.results.append(result) // Record the result
}
self.group.leave() // This task is done
}
}
func getResults() -> [Result] {
group.wait() // Make sure all in-flight tasks are done
return resultAggregatorQueue.sync { return results }
}
}
let taskQueue = DispatchQueue(label: "Task Queue", attributes: .concurrent)
let taskRunner = ParallelTaskRunner<Int>()
for i in 0...100 {
taskRunner.execute { completionHandler in
taskQueue.async { // Simulated async computation
let randomTime = 3.0
print("Sleeping for \(randomTime)")
Thread.sleep(forTimeInterval: randomTime) // Simulates intesnive computation
let result = i // Simulate a result
completionHandler(result)
}
}
}
print(taskRunner.getResults()) // Oh look, all the results are here! :D
I'm using RxSwift with MVVM and I'm found myself a bit confused. Here's why:
My Code Right Now
ViewModel
internal protocol DetailViewModelInput {
func viewDidLoad(with name: String)
}
internal protocol DetailViewModelOutput {
var gnomeObject: Observable<Gnome?> { get }
}
struct DetailViewModel: DetailViewModelType, DetailViewModelInput, DetailViewModelOutput {
let disposeBag = DisposeBag()
let gnomeObject: Observable<Gnome?>
init() {
gnomeObject = viewDidLoadProperty
.asObservable()
.filter { !$0.isEmpty }
.map { guard let gnome = Gnome
.fetch(uniqueValue: $0, forKey: "name")! as? Gnome else { return nil }
return gnome
}
}
let viewDidLoadProperty = Variable<String>("")
func viewDidLoad(with name: String) {
viewDidLoadProperty.value = name
}
}
ViewController
I make the binding as follows:
func bindViewModel() {
viewModel.outputs.gnomeObject
.subscribe { observable in self.populate(with: observable.element != nil ? observable.element! : nil) }
.addDisposableTo(viewModel.disposeBag)
}
And this is "fine". It works perfectly (at least as expected).
But, I while reading the following book: https://victorqi.gitbooks.io/rxswift/content/tips.html
In the tips section it says:
Always strive to model your systems or their parts as pure functions. Those pure functions can be tested easily and can be used to modify operator behaviors.
And after reading it I'm changed my ViewModel as follows:
ViewModel (Edited)
internal protocol DetailViewModelInput {
func viewDidLoad(with name: String)
}
internal protocol DetailViewModelOutput {
func gnomeObject() -> Observable<Gnome?>
}
protocol DetailViewModelType {
var disposeBag: DisposeBag { get }
var inputs: DetailViewModelInput { get }
var outputs: DetailViewModelOutput { get }
}
struct DetailViewModel: DetailViewModelType, DetailViewModelInput {
let disposeBag = DisposeBag()
let viewDidLoadProperty = Variable<String>("")
func viewDidLoad(with name: String) {
viewDidLoadProperty.value = name
}
}
// MARK: DetailViewModelOutput
extension DetailViewModel: DetailViewModelOutput {
func gnomeObject() -> Observable<Gnome?> {
return viewDidLoadProperty
.asObservable()
.filter { !$0.isEmpty }
.map { guard let gnome = Gnome
.fetch(uniqueValue: $0, forKey: "name")! as? Gnome else { return nil }
return gnome
}
}
}
The difference in the ViewModels is the GnomeObject declaration, in one it is a var and in the "edited" is a func.
My concern is, that every time gnomeObject() gets called from the ViewController, it will create a new instance of the observable.
What should be the best practice in this case?
Hmm, in the first version, gnomeObject is a let, not a var. Once it is set, it is never changed to a different object.
In the second version gnomeObject() returns a different object every time it's called. So this actually breaks the "pure function" paradigm. (Note: if the Observable was a struct instead of a class then this wouldn't be the case because structs don't have identity.)
Your first example follows the pure function concept while your second version breaks it.
If you're looking to eliminate the need to instantiate gnomeObject in the initializer, you could modify the first example to use a lazy var like so:
lazy var gnomeObject: Observable<Gnome?> = self.viewDidLoadProperty
.asObservable()
.filter { !$0.isEmpty }
.map { guard let gnome = Gnome
.fetch(uniqueValue: $0, forKey: "name")! as? Gnome else { return nil }
return gnome
}
When they say you should use pure functions they mean that functions (when possible) should have the same output for the same set of inputs, meaning, if a function is called twice with the same set of inputs it should return the same thing twice.
That means you don't have any hidden mutable state that the caller of the functions is not aware of (a property in the class that owns the method, for example). Everything should be as explicit as possible.
So, it's something you should be aware of when it comes to functions. But it's completely ok to use properties, as you were doing in the first code, they don't apply to this.