How can I assert a delegate is called in my unit test - ios

I would like to asset that the correct delegate method is called depending on the result of a check in my presenter.
Having mocked out my IdentityProvider to return true, how would I write a test to assert delegate?.userIsAuthenticated() is called?
import Foundation
import InjectStory
protocol StartPresenterDelegate: class {
func userIsAuthenticated()
func userNeedsToAuthenticate()
}
class StartPresenter {
weak var delegate: StartPresenterDelegate?
weak var view: StartViewInterface!
private lazy var identityProvider = Dependencies.identityProvider.inject()
init(view: StartViewInterface) {
self.view = view
}
private func checkUserAuthState() {
if identityProvider.isAuthorized() {
delegate?.userIsAuthenticated()
} else {
delegate?.userNeedsToAuthenticate()
}
}
}
extension StartPresenter: StartPresentation {
func onViewDidLoad() {
checkUserAuthState()
}
}
extension StartPresenter {
struct Dependencies {
static let identityProvider = Injection<IdentityProviderProtocol>(IdentityProvider.shared)
}
}

You need to do some trick. Create MockDelegateClass for your protocol StartPresenterDelegate
example:
class MockDelegate: StartPresenterDelegate {
var isUserIsAuthenticatedCalled = false
var isUserNeedsToAuthenticateCalled = false
func userIsAuthenticated() {
isUserIsAuthenticatedCalled = true
}
func userNeedsToAuthenticate() {
isUserNeedsToAuthenticateCalled = true
}
}
then in your test try to do something like that:
func test_MyTest() {
// init your class StartPresenter that you wanna test
var presenter = StartPresenter(...)
var mockDelegate = MockDelegate()
presenter.delegate = mockDelegate
presenter.onViewDidLoad()
XCTAssertTrue(mockDelegate.isUserIsAuthenticatedCalled)
XCTAssertFalse(mockDelegate.isUserNeedsToAuthenticateCalled)
// or change isUserIsAuthenticatedCalled and isUserNeedsToAuthenticateCalled if you expect another states
}
For different states you need different tests, for you it will be the easiest way to test delegate calling.

You have to mock the StartPresenterDelegate as follows too.
class MockStartPresenterDelegate: StartPresenterDelegate {
var userIsAuthenticated_wasCalled = false
func userIsAuthenticated() {
userIsAuthenticated_wasCalled = true
}
}
Inject MockStartPresenterDelegate as the delegate and check that userIsAuthenticated_wasCalled is true

In this way you can confirm the unit test for delegates in Swift apps

Related

iOS - Unit Testing asynchronous private function in Presenter of MVP

Hello I'm trying to unit testing a private function which located in Presenter
This is my Presenter Codes and I'm using Networking Singleton Object APIService
class MyPresenter {
weak var vc: MyProtocol?
func attachView(vc: MyProtocol?) {
self.vc = vc
}
func request(_ id: String) {
if id.count == 0 {
vc?.showIDEmptyAlert()
return
}
fetch(id)
}
private func fetch(_ id:String) {
DispatchQueue.global.async {
APIService.shared.fetch(id) { (data, err) in
if let err = err {
self.vc?.showErrorAlert()
return
}
self.vc?.update(data)
}
}
}
}
and this is my ViewController codes
class MyViewController: UIViewController, MyProtocol {
private var presenter: MyPresenter = MyPresenter()
override func viewDidLoad() {
super.viewDidLoad()
presenter.attachView(vc: self)
}
func showIDEmptyAlert() {
self.present ..
}
func showErrorAlert() {
self.present ..
}
func update(data: String) {
self.label.text = data
}
#IBAction func handleRegisterButton(_ sender: UIButton) {
guard let id = idTextField.text else { return }
presenter.request(id)
}
}
These are my Presenter and View. And I wrote Test Code like this
First, I made Mock PassiveView Like this
class MyViewMock: MyProtocol {
private (set) var showEmptyIdAlertHasBeenCalled = false
private (set) var showErrorAlertHasBeenCalled = false
private (set) var updateHasBeenCalled = false
func showEmptyIdAlert() {
showEmptyIdAlertHasBeenCalled = true
}
func showErrorAlert() {
showErrorAlertHasBeenCalled = true
}
func update(data: String) {
updateHasBeenCalled = true
}
}
So I expected that if I could test Presenter's request(_:) methods with valid id and invalid
but because request(_:) didn't get handler parameter and APIService.shared.fetch is asynchronous, I couldn't get
correct result by calling request(_:). (Always false)
How can I test this kind of Presenter?
In terms ofXCTests, there is the XCTestExpectation class to test asynchronous functions.
But there is an issue in your approach of testing the Presenter. You should use mock for your network service and stub it with expected arguments. It doesn't make sense to call the actual service. Unit tests are the fastest kind of tests. Private methods are a part of black-box which you should not care about its internal structure. Test public interfaces but don't test private methods.
If you will try to mock and stub APIService, then you notice it's impossible to do if it's a singleton. You'll end up with injecting service as a dependency into Presenter for the better testability.
If the service will be mocked and the stub will be used, then there is no need in using XCTestExpectation, because there won't be any asynchronous code.
To test asynchronous methods, you must use XCTestExpectation to make your test wait for the asynchronous operation to complete.
it's a test asynchronous method example
// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
// given
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
// 1
let promise = expectation(description: "Status code: 200")
// when
let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
// then
if let error = error {
XCTFail("Error: \(error.localizedDescription)")
return
} else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if statusCode == 200 {
// 2
promise.fulfill()
} else {
XCTFail("Status code: \(statusCode)")
}
}
}
dataTask.resume()
// 3
waitForExpectations(timeout: 5, handler: nil)
}
You can read more about Unit test from here

NSKeyValueObservation not working, Swift 4

How should I properly set up to receive notifications when the variable changes on the shared instance of the singleton? Currently, the block is never fired.
class MyViewController: UIViewController {
private var observer: NSKeyValueObservation?
func configureKVO() {
observer = MySingleton.shared.observe(\.shouldFetchDataFromServer) { (manager, change) in
print("Changed: \(manager.shouldFetchDataFromServer)")
}
MySingleton.shared.shouldFetchDataFromServer = false
MySingleton.shared.shouldFetchDataFromServer = true
}
}
class MySingleton: NSObject {
static let shared = MySingleton()
#objc var shouldFetchDataFromServer: Bool = false
}
Add the 'dynamic' keyword to the declaration of shouldFetchDataFromServer, and it should work.

Subscribing to a property

I have a singleton service class which maintains a value of heading it gets from the compass. I have a UIView which draws some custom graphics based on this. I'm trying to do something like an Observable in javascript, where my code gets executed when there's a change in the value.
final class LocationService: NSObject {
static let shared = LocationService()
public var heading:Int
public func getHeading() -> Int {
return self.heading
}
Then in my UIView subclass:
var ls:LocationService = LocationService.shared
var heading: Int = ls.getHeading() {
didSet {
setNeedsDisplay()
}
}
I tried also just directly accessing the property via ls.heading but this doesn't get accepted either. It's telling me I cannot use the instance member within the property initialiser. What's a proper swift method of doing this?
Edit:
I've been working with Christian's answer below and some other documentation and now got to here where it all compiles nicely, but doesn't actually work. Here's my delegator and protocol:
final class LocationService: NSObject {
static let shared = LocationService()
weak var delegate: CompassView?
var heading:Int
func headingUpdate(request:HeadingRequest, updateHeading:CLHeading) {
print ("New heading found: \(updateHeading)")
self.heading = Int(updateHeading.magneticHeading)
self.delegate?.setHeading(newHeading: Int(updateHeading.magneticHeading))
}
public func getHeading() -> Int {
return self.heading
}
}
protocol LSDelegate: class {
func setHeading(newHeading:Int)
}
Then in the delegate:
class CompassView: UIView, LSDelegate {
func setHeading(newHeading:Int) {
self.heading = newHeading
print("heading updated in compass view to \(self.heading)")
setNeedsDisplay()
}
}
So I get the print message that the heading has been updated in the headingUpdate function. The print message in the setHeading function in the delegate CompassView never gets displayed.
You can use the delegation pattern and have that class that wants to consume your events implement the functions in your protocol.
protocol MyDelegate {
func setNeedsDisplay()
}
class LocationService: NSObject {
var myDelegate : MyDelegate?
var heading: Int = ls.getHeading() {
didSet {
myDelegate?.setNeedsDisplay()
}
}
...
func assignDelegate() {
self.myDelegate = MyConsumer()
}
}
class MyConsumer : MyDelegate {
func setNeedsDisplay()
{
}
}

Chromecast listener is not working when i create seperate class from Controller

Created class:
class ChromeCast:NSObject {
//MARK:Chromecast Vars
var applicationMetadata: GCKApplicationMetadata?
var selectedDevice: GCKDevice?
var deviceManager: GCKDeviceManager?
var mediaInformation: GCKMediaInformation?
var mediaControlChannel: GCKMediaControlChannel?
var deviceScanner: GCKDeviceScanner
lazy var kReceiverAppID:String = {
// You can add your own app id here that you get by registering with the
// Google Cast SDK Developer Console https://cast.google.com/publish
return kGCKMediaDefaultReceiverApplicationID
}()
override init() {
let filterCriteria = GCKFilterCriteria(forAvailableApplicationWithID:
kGCKMediaDefaultReceiverApplicationID)
deviceScanner = GCKDeviceScanner(filterCriteria:filterCriteria)
}
func startChromeCastScanning() {
// Initialize device scanner
deviceScanner.addListener(self)
deviceScanner.startScan()
deviceScanner.passiveScan = false
Log.Info("Start Scanning")
}
}
extension ChromeCast: GCKDeviceScannerListener {
func deviceDidComeOnline(device: GCKDevice!) {
Log.Info("Device found: \(device.friendlyName)")
}
func deviceDidGoOffline(device: GCKDevice!) {
Log.Info("Device went away: \(device.friendlyName)")
}
}
and calling it from my view controller as:
someFunctionInViewController() {
let chrome = ChromeCast()
chrome.startChromeCastScanning()
}
Delegate is not getting called back . when device discovered.
But same works when i add all properties and function in view controller it self.
But i want to seperate it from controller. what is the issue?
I Figured out the problem
instead of local i declare global property for Chromecast in my controller
var chrome : ChromeCast?
and then from my function i called it as
func someFunction() {
chrome = ChromeCast()
chrome?.startChromeCastScanning()
}

Delegate method property to being triggered on conforming class

I have the following protocol
protocol SentenceDelegate: class{
func sentenceDidFinish()
}
I have my SentenceMarkov class conform to the protocol:
class SentenceMarkov : SentenceDelegate{
// foo
// bar
}
I implement the protocol method in my conforming class:
class SentenceMarkov : SentenceDelegate{
//...
func sentenceDidFinish{
//Do something
}
//...
}
I create a property on the class that calls the protocol method called sentenceDelegate :
class otherClass{
//..
weak var sentenceDelegate: SentenceDelegate?
//..
}
I set this property in my first class to self
class SentenceMarkov{
var FirstOne:OtherClass {
didSet { FirstOne.sentenceDelegate = self}
}
var SecondOne:OtherClass {
didSet{ SecondOne.sentenceDelegate = self}
}
init(Ult:OtherClass, Penult:OtherClass){
self.FirstOne= Ult
self.SecondOne = Penult
self.FirstOne.sentenceDelegate = self
self.SecondOne.sentenceDelegate = self
}
//..
}
Finally I call the sentenceDelegate method in OtherClass after its init()
class OtherClass{
func sentenceDone(){
sentenceDelegate?.sentenceDidFinish()
}
}
The problem is that, when I set breakpoints on the above method sentenceDelegate is nil. I am not sure why because I set it, although I could be setting it wrong, I am not sure how to ameliorate that though. Here is the three Swift files include the ViewController where SentenceMarkov is initialized:
https://gist.github.com/ebbnormal/e1cb791dd165a6866e11
https://gist.github.com/ebbnormal/263c9343e403c3a7ac40
https://gist.github.com/ebbnormal/1400e7da024d78ba5ed0
The following works for me:
protocol Greeter: class {
func greet()
}
class Dog {
weak var greeter: Greeter?
}
class DogPound: Greeter {
func greet() {
print("Hi. Welcome to the DogPound!")
}
var dog: Dog {
didSet {
dog.greeter = self
}
}
init(dog: Dog) {
self.dog = dog
self.dog.greeter = self
}
}
let myDog = Dog()
myDog.greeter?.greet() //=>nil
DogPound(dog: myDog)
myDog.greeter?.greet()
--output:--
Hi. Welcome to the DogPound!

Resources