Swift - using XCTest to test function containing closure - ios

I am fairly new to Swift and am currently trying to write a unit test (using XCTest) to test the following function:
func login(email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
if let _error = error {
print(_error.localizedDescription)
} else {
self.performSegue(identifier: "loginSeg")
}
}
}
My research has identified that I need to use the XCTestExpectation functionality as XCTest executes synchronously by default meaning it won't wait for the closure to finish running (please correct me if I'm wrong).
Whats throwing me off is how I test the login function as it itself calls the asynchronous function Auth.auth().signIn(). I'm trying to test whether the signIn is successful.
Apologies if this has already been answered but I couldn't find an answer that directly addresses this issue.
Thanks
Update:
With some help from the answers and further research I amended by login function to use an escaping closure:
func login(email: String, password: String, completion: #escaping(Bool)->()) {
Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
if let _error = error {
print(_error.localizedDescription)
completion(false)
} else {
self.performSegue(identifier: "loginSeg")
completion(true)
}
}
}
I then test in the following way:
func testLoginSuccess() {
// other setup
let exp = expectation(description: "Check Login is successful")
let result = login.login(email: email, password: password) { (loginRes) in
loginResult = loginRes
exp.fulfill()
}
waitForExpectations(timeout: 10) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
XCTAssertEqual(loginResult, true)
}
}
My test function now tests the login functionality successfully.
Hope this helps someone as it left me stumped for a while :)

The call to Auth is an architectural boundary. Unit tests are faster and more reliable if they go up to such boundaries, but don't cross them. We can do this by isolating the Auth singleton behind a protocol.
I'm guessing at the signature of signIn. Whatever it is, copy and paste it into a protocol:
protocol AuthProtocol {
func signIn(withEmail email: String, password: String, completion: #escaping (String, NSError?) -> Void)
}
This acts as a thin slice of the full Auth interface, taking only the part you want. This is an example of the Interface Segregation Principle.
Then extend Auth to conform to this protocol. It already does, so the conformance is empty.
extension Auth: AuthProtocol {}
Now in your view controller, extract the direct call to Auth.auth() into a property with a default value:
var auth: AuthProtocol = Auth.auth()
Talk to this property instead of directly to Auth.auth():
auth.signIn(withEmail: email, …etc…
This introduces a Seam. A test can replace auth with an implementation that is a Test Spy, recording how signIn is called.
final class SpyAuth: AuthProtocol {
private(set) var signInCallCount = 0
private(set) var signInArgsEmail: [String] = []
private(set) var signInArgsPassword: [String] = []
private(set) var signInArgsCompletion: [(String, Foundation.NSError?) -> Void] = []
func signIn(withEmail email: String, password: String, completion: #escaping (String, Foundation.NSError?) -> Void) {
signInCallCount += 1
signInArgsEmail.append(email)
signInArgsPassword.append(password)
signInArgsCompletion.append(completion)
}
}
A test can inject the SpyAuth into the view controller, intercepting everything that would normally go to Auth. As you can see, this includes the completion closure. I would write
One test to confirm the call count and the non-closure arguments
Another test to get the captured closure and call it with success.
I'd also call it with failure, if your code didn't have a print(_) statement.
Finally, there's the matter of segues. Apple hasn't given us any way to unit test them. As a workaround, you can make a partial mock. Something like this:
final class TestableLoginViewController: LoginViewController {
private(set) var performSegueCallCount = 0
private(set) var performSegueArgsIdentifier: [String] = []
private(set) var performSegueArgsSender: [Any?] = []
override func performSegue(withIdentifier identifier: String, sender: Any?) {
performSegueCallCount += 1
performSegueArgsIdentifier.append(identifier)
performSegueArgsSender.append(sender)
}
}
With this, you can intercept calls to performSegue. This isn't ideal, because it's a legacy code technique. But it should get you started.
final class LoginViewControllerTests: XCTestCase {
private var sut: TestableLoginViewController!
private var spyAuth: SpyAuth!
override func setUp() {
super.setUp()
sut = TestableLoginViewController()
spyAuth = SpyAuth()
sut.auth = spyAuth
}
override func tearDown() {
sut = nil
spyAuth = nil
super.tearDown()
}
func test_login_shouldCallAuthSignIn() {
sut.login(email: "EMAIL", password: "PASSWORD")
XCTAssertEqual(spyAuth.signInCallCount, 1, "call count")
XCTAssertEqual(spyAuth.signInArgsEmail.first, "EMAIL", "email")
XCTAssertEqual(spyAuth.signInArgsPassword.first, "PASSWORD", "password")
}
func test_login_withSuccess_shouldPerformSegue() {
sut.login(email: "EMAIL", password: "PASSWORD")
let completion = spyAuth.signInArgsCompletion.first
completion?("DUMMY", nil)
XCTAssertEqual(sut.performSegueCallCount, 1, "call count")
XCTAssertEqual(sut.performSegueArgsIdentifier.first, "loginSeg", "identifier")
let sender = sut.performSegueArgsSender.first
XCTAssertTrue(sender as? TestableLoginViewController === sut,
"Expected sender \(sut!), but was \(String(describing: sender))")
}
}
Absolutely nothing asynchronous here, so no waitForExpectations. We capture the closure, we call the closure.

Jon's answer is excellent, I can't add comments yet, so I'll add my advice here.
For those who have (for any reason) a static/class function instead of a singleton or an instance function, this could help you:
For example, if you have Auth.signIn(withEmail: emai... where signIn is a static function. Instead of use:
var auth: AuthProtocol = Auth.auth()
Use:
var auth: AuthProtocol.Type = Auth.self
And assign it like this
sut.auth = SpyAuth.self

Related

Returning Conditional Responses from MockedWebService in Swift

I am implementing UI Testing and need to mock a service so I don't call the service again and again and remove the dependency on the network call. So, I created a Mock called MockWebservice. It is implemented below:
class MockedWebservice: NetworkService {
func login(username: String, password: String, completion: #escaping (Result<LoginResponse?, NetworkError>) -> Void) {
completion(.success(LoginResponse(success: true)))
}
}
It works but as you can see it always returns success: true. How can I make this MockedWebservice return a different response. The MockWebservice is injected into the main app using the launchEnvironment for unit test. Here is the code in the actual SwiftUI App which creates a real web service or a mocked version.
class NetworkServiceFactory {
static func create() -> NetworkService {
let environment = ProcessInfo.processInfo.environment["ENV"]
if let environment = environment {
if environment == "TEST" {
return MockedWebservice()
} else {
return Webservice()
}
} else {
return Webservice()
}
}
}
Add some logic to your mocked service so that it responds differently depending on the username/password it receives
Something like:
class MockedWebservice: NetworkService {
func login(username: String, password: String, completion: #escaping (Result<LoginResponse?, NetworkError>) -> Void) {
if username == "success" {
completion(.success(LoginResponse(success: true)))
} else {
completion(.failure(SomeNetworkError()))
}
}
}
You can test for additional username values to simulate different responses.
I would probably make the mocked method a bit more realistic. Use an asyncAfter on a utility dispatch queue to simulate network latency and the fact that your completion handler probably wont be called on the main queue.
class MockedWebservice: NetworkService {
func login(username: String, password: String, completion: #escaping (Result<LoginResponse?, NetworkError>) -> Void) {
DispatchQueue.global(qos: .utility).asyncAfter(.now()+0.5) {
if username == "success" {
completion(.success(LoginResponse(success: true)))
} else {
completion(.failure(SomeNetworkError()))
}
}
}
}

Mocking iOS Firebase Auth sign-in methods

This question is somewhat similar to Mock third party classes (Firebase) in Swift but different enough to warrant a new question, based on the answers to it.
I'm trying to mock the Auth/FIRAuth method signIn(withEmail email: String, password: String, completion: AuthDataResultCallback?) and am running into difficulties with trying to mock the AuthDataResultCallback object, mainly because it has a User property that I also want to mock. Unfortunately, I'm not able to create my own User or Auth objects because they've been marked as not having an available initializer in Swift.
I have an object (let's call it UserAuthenticationRepository) that's responsible for performing user authentication and database reads. I'd like to inject a Firebase auth object into it to do these things under the hood, but since I want to test this repository object I'd like to be able to inject a Firebase mock object when I go to unit test it.
What I want to do is something like this (simplified slightly for this question):
import FirebaseAuth
protocol FirebaseUserType {
var uid: String { get }
}
extension User: FirebaseUserType {}
protocol FirebaseAuthDataResultType {
var user: FirebaseUserType { get }
}
extension AuthDataResult: FirebaseAuthDataResultType {
var user: FirebaseUserType {
// This is where I'm running into problems because AuthDataResult expects a User object,
// which I also use in the UserAuthenticationRepository signIn(withEmail:) method
}
}
protocol FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: ((FirebaseAuthDataResultType?, Error?) -> Void)?)
}
extension Auth: FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: ((FirebaseAuthDataResultType?, Error?) -> Void)?) {
let completion = completion as AuthDataResultCallback?
signIn(withEmail: email, password: password, completion: completion)
}
}
protocol UserAuthenticationType {
func loginUser(emailAddress: String, password: String) -> Observable<User>
}
class UserAuthenticationRepository: UserAuthenticationType {
private let authenticationService: FirebaseAuthenticationType
private let disposeBag = DisposeBag()
init(authenticationService: FirebaseAuthenticationType = Auth.auth()) {
self.authenticationService = authenticationService
}
func loginUser(emailAddress: String, password: String) -> Observable<User> {
return .create { [weak self] observer in
self?.authenticationService.signIn(withEmail: emailAddress, password: password, completion: { authDataResult, error in
if let error = error {
observer.onError(error)
} else if let authDataResult = authDataResult {
observer.onNext(authDataResult.user)
}
})
return Disposables.create()
}
}
As noted above, I'm running into problems when I try to extend AuthDataResult to conform to my FirebaseAuthDataResultType protocol. Is it possible to do what I'm trying to do? I'd ultimately like to pass back a uid string in my Firebase authentication service when testing UserAuthenticationRepository.
I was eventually able to find a way to mock the Firebase Auth objects necessary for me, but I had to resort to subclassing the Firebase User object, adding new properties to it be used during testing (can't create a User object directly or mutate its properties), and then creating a struct which conforms to FirebaseAuthDataResultType which is initialized with a MockUser object during testing. The protocols and extensions I ended up needing are below:
protocol FirebaseAuthDataResultType {
var user: User { get }
}
extension AuthDataResult: FirebaseAuthDataResultType {}
typealias FirebaseAuthDataResultTypeCallback = (FirebaseAuthDataResultType?, Error?) -> Void
protocol FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: FirebaseAuthDataResultTypeCallback?)
func signOut() throws
func addStateDidChangeListener(_ listener: #escaping AuthStateDidChangeListenerBlock) -> AuthStateDidChangeListenerHandle
func removeStateDidChangeListener(_ listenerHandle: AuthStateDidChangeListenerHandle)
}
extension Auth: FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: FirebaseAuthDataResultTypeCallback?) {
let completion = completion as AuthDataResultCallback?
signIn(withEmail: email, password: password, completion: completion)
}
}
Below are the mock objects:
class MockUser: User {
let testingUID: String
let testingEmail: String?
let testingDisplayName: String?
init(testingUID: String,
testingEmail: String? = nil,
testingDisplayName: String? = nil) {
self.testingUID = testingUID
self.testingEmail = testingEmail
self.testingDisplayName = testingDisplayName
}
}
struct MockFirebaseAuthDataResult: FirebaseAuthDataResultType {
var user: User
}
An instance of my mock Firebase authentication service with stubs:
class MockFirebaseAuthenticationService: FirebaseAuthenticationType {
typealias AuthDataResultType = (authDataResult: FirebaseAuthDataResultType?, error: Error?)
var authDataResultFactory: (() -> (AuthDataResultType))?
func signIn(withEmail email: String, password: String, completion: FirebaseAuthDataResultTypeCallback?) {
// Mock service logic goes here
}
// ...rest of protocol functions
}
Usage (using RxSwift and RxTest):
func testLoginUserReturnsUserIfSignInSuccessful() {
let firebaseAuthService = MockFirebaseAuthenticationService()
let expectedUID = "aM1RyjpaZcQ4EhaUvDAeCnla3HX2"
firebaseAuthService.authDataResultFactory = {
let user = MockUser(testingUID: expectedUID)
let authDataResult = MockFirebaseAuthDataResult(user: user)
return (authDataResult, nil)
}
let sut = UserSessionRepository(authenticationService: firebaseAuthService)
let userObserver = testScheduler.createObserver(User.self)
sut.loginUser(emailAddress: "john#gmail.com", password: "123456")
.bind(to: userObserver)
.disposed(by: disposeBag)
testScheduler.start()
let user = userObserver.events[0].value.element as? MockUser
// Assert MockUser properties, events, etc.
}
If anyone has any better ideas of how this can be accomplished, please let me know!

query regarding mocking singleton in swift ,ios using xctest?

this is not a question regarding that should we use singleton or not. but rather mocking singleton related.
this is just a sample example, as i was reading about mocking singleton is tough. so i thought let me give a try.
i am able to mock it but not sure is this a correct approach ?
protocol APIManagerProtocol {
static var sharedManager: APIManagerProtocol {get set}
func doThis()
}
class APIManager: APIManagerProtocol {
static var sharedManager: APIManagerProtocol = APIManager()
private init() {
}
func doThis() {
}
}
class ViewController: UIViewController {
private var apiManager: APIManagerProtocol?
override func viewDidLoad() {
}
convenience init(_ apimanager: APIManagerProtocol){
self.init()
apiManager = apimanager
}
func DoSomeRandomStuff(){
apiManager?.doThis()
}
}
import Foundation
#testable import SingleTonUnitTesting
class MockAPIManager: APIManagerProtocol {
static var sharedManager: APIManagerProtocol = MockAPIManager()
var isdoThisCalled = false
func doThis(){
isdoThisCalled = true
}
private init(){
}
}
class ViewControllerTests: XCTestCase {
var sut: ViewController?
var mockAPIManager: MockAPIManager?
override func setUp() {
mockAPIManager = MockAPIManager.sharedManager as? MockAPIManager
sut = ViewController(mockAPIManager!)
}
func test_viewController_doSomeRandomStuffs(){
sut?.DoSomeRandomStuff()
XCTAssertTrue(mockAPIManager!.isdoThisCalled)
}
override func tearDown() {
sut = nil
mockAPIManager = nil
}
}
The basic idea is right: Avoid repeated references to the singleton directly throughout the code, but rather inject object that conforms to the protocol.
What’s not quite right is that you are testing something internal to the MockAPIManager class. The mock is only there to serve a broader goal, namely to test your business logic (without external dependencies). So, ideally, you should be testing something that is exposed by APIManagerProtocol (or some logical result of that).
So, let’s make this concrete: For example, let’s assume your API had some method to retrieve the age of a user from a web service:
public protocol APIManagerProtocol {
func fetchAge(for userid: String, completion: #escaping (Result<Int, Error>) -> Void)
}
(Note, by the way, that the static singleton method doesn’t belong in the protocol. It’s an implementation detail of the API manager, not part of the protocol. No controllers that get a manager injected will ever need to call shared/sharedManager themselves.)
And lets assume that your view controller (or perhaps better, its view model/presenter) had a method to retrieve the age and create an appropriate message to be shown in the UI:
func buildAgeMessage(for userid: String, completion: #escaping (String) -> Void) {
apiManager?.fetchAge(for: userid) { result in
switch result {
case .failure:
completion("Error retrieving age.")
case .success(let age):
completion("The user is \(age) years old.")
}
}
}
The API manager mock would then implement the method:
class MockAPIManager: APIManagerProtocol {
func fetchAge(for userid: String, completion: #escaping (Result<Int, Error>) -> Void) {
switch userid {
case "123":
completion(.success(42))
default:
completion(.failure(APIManagerError.notFound))
}
}
}
Then you could test the logic of building this string to be shown in your UI, using the mocked API rather than the actual network service:
class ViewControllerTests: XCTestCase {
var viewController: ViewController?
override func setUp() {
viewController = ViewController(MockAPIManager())
}
func testSuccessfulAgeMessage() {
let e = expectation(description: "testSuccessfulAgeMessage")
viewController?.buildAgeMessage(for: "123") { string in
XCTAssertEqual(string, "The user is 42 years old.")
e.fulfill()
}
waitForExpectations(timeout: 1)
}
func testFailureAgeMessage() {
let e = expectation(description: "testFailureAgeMessage")
viewController?.buildAgeMessage(for: "xyz") { string in
XCTAssertEqual(string, "Error retrieving age.")
e.fulfill()
}
waitForExpectations(timeout: 1)
}
}
i was reading about mocking singleton is tough
The notion is that if you have these APIManager.shared references sprinkled throughout your code, it’s harder to swap them out with the mock object. Injecting solves this problem.
Then, again, if you’ve now injected this APIManager instance everywhere to facilitate mocking and have eliminate all of these shared references, it begs the question that you wanted to avoid, namely why use a singleton anymore?

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

iOS Swift Networking Layer

So I'm following a git repo for handling network requests. I have successfully implemented it into my application, followed guide.
I'm calling the SignInOperation as follows in my SignInController: SignInOperation(email: email, password: password).start() So the way that this repo is setup to mainly handle the success and the failure in the RequestOperation as shown below:
import Foundation
public class SignInOperation: ServiceOperation {
private let request: SignInRequest
public var success: ((SignInItem) -> Void)?
public var failure: ((NSError) -> Void)?
public init(email: String, password: String, service: BackendService = MyBackendService(BackendConfiguration.shared)) {
request = SignInRequest(email: email, password: password)
super.init(service: service)
}
public override func start() {
super.start()
service.request(request, success: handleSuccess, failure: handleFailure)
}
private func handleSuccess(_ response: Any?) {
do {
let item = try SignInResponseMapper.process(response)
self.success?(item)
self.finish()
} catch {
handleFailure(NSError.cannotParseResponse())
}
}
private func handleFailure(_ error: NSError) {
self.failure?(error)
self.finish()
}
}
Mainly what I'm tryin to do is something like:
SignInOperation(email: email, password: password).start().then(
// handleResponse
)
Not even necessarily like that. But just a way I can handle the response in my controller and not network file. Any suggestions or ideas would be greatly appreciated. I can DEFINITELY share more code if one feels it to be necessary.
PS. I am specifically trying to follow this design of handling your Network Requests because I'm building a more large scale social app. Therefor, I want something that is maintainable, scalable and testable.
The operations success and error state is passed to closures call the closures to handle. hope below code will help you.
class Test : UIViewController{
let mail = "abc#xyz.com"
let password = "******"
var operation : SignInOperation?
override func viewDidLoad() {
super.viewDidLoad()
operation = SignInOperation(email: mail, password: password)
operation?.failure = { error in
print(error.localizedDescription)
// handle failure over here
}
operation?.success = { item in
// handle success here
// you can use data from item which is an Instance of SignInItem over here
}
}
}

Resources