Mocking iOS Firebase Auth sign-in methods - ios

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!

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()))
}
}
}
}

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?

Swift - using XCTest to test function containing closure

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

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
}
}
}

Cannot set value conforming to protocol to property with protocol type

I'm trying to create a fake authenticator for my unit tests that can manually set the user as logged in or logged out and bypass the API my code actually uses where I'd need a real accessToken to log the user in.
I've wrapped the Authentication API my app uses in to the following class:
API Wrapper
import OIDC
protocol Authenticator {
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void)
}
struct OIDCAuthenticator: Authenticator {
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void) {
//API call
OIDCHelper.getValidAccessToken { (error, accessToken) in
DispatchQueue.main.async {
completionHandler( error, accessToken)
}
}
}
}
Then I create a Fake Authenticator using the same protocol for testing purposes
Fake/Testing Authenticator
import OIDC
import Foundation
///Mocks the user being logged in our logged out for Testing purposes
struct FakeAuthenticator: Authenticator {
let error: OIDCError?
let accessToken: String?
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void) {
completionHandler(error, accessToken)
}
init(loggedIn: Bool) {
if loggedIn {
error = .tokenNotFoundInKeychainData
accessToken = nil
} else {
error = nil
accessToken = "abcdefg"
}
}
}
Settings the OIDCAuthenticator API Wrapper works fine when settings the authenticator in the ViewController subclass.
TableViewController Implementation
import UIKit
import OIDC
class SettingsPageTableViewController: UITableViewController{
// MARK: - Outlets and variables
var authenticator: Authenticator!
private var isUserLoggedIn = false
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
authenticator = OIDCAuthenticator()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
authenticator.getValidAccessToken { [weak self] (error, _) in
self?.isUserLoggedIn = (error == nil)
self?.setLogginStatus()
}
}
}
However when I try to do the same thing with the FakeAuthenticator in my Unit tests I get the following error:
SettingsViewController Test Class
import XCTest
import UIKit
#testable import MyProject
class SettingsViewControllerTests: XCTestCase {
var viewController: SettingsPageTableViewController!
override func setUp() {
super.setUp()
configureViewControllerForTesting()
}
private func configureViewControllerForTesting() {
let storyboard = UIStoryboard(name: "SettingsPage", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
viewController = navigationController.topViewController as! SettingsPageTableViewController
_ = viewController.view
}
func testSignInButtonIsAvailableWhenUnauthenticated() {
viewController.authenticator = FakeAuthenticator(loggedIn: false)
}
}
The same things happens when I swap out FakeAuthenticator with OIDCAuthenticator. I've also attempted to cast the FakeAuthenticator to Authenticator but this merely alters the Error to Cannot assign value of type 'Authenticator' to type 'Authenticator!'.
Why am I getting this error and what is the best approach to fixing this?
You need to remove the files from your test target since you're already importing your whole project with #testable.

Resources