Test for a UIViewController fails after refactor in Swift - ios

I'm having different results in trying to test a ViewController in Swift.
This first code pass the test.
#testable import VideoAudioExtractor
import XCTest
class SecondViewControllerTest: XCTestCase {
let storyBoardName = "Main"
let viewControllerIdentifier = "SecondViewController"
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
func testSelectAudioButtonIsConnected () {
let sut = UIStoryboard(name: storyBoardName, bundle: nil).instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
let dummy = sut.view
if let unpwarppedOptional = sut.selectAudioButton {
XCTAssertEqual(unpwarppedOptional,sut.selectAudioButton, "correct value")
}
else {
XCTFail("Value isn't set")
}
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
}
If I refactor the test and I move the creation of the view controller to an instance variable the test fails in Line
#testable import VideoAudioExtractor
import XCTest
class SecondViewControllerTest: XCTestCase {
let storyBoardName = "Main"
let viewControllerIdentifier = "SecondViewController"
var sut : SecondViewController {
return UIStoryboard(name: storyBoardName, bundle: nil).instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
}
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
func testSelectAudioButtonIsConnected () {
let dummy = sut.view
if let unpwarppedOptional = sut.selectAudioButton {
XCTAssertEqual(unpwarppedOptional,sut.selectAudioButton, "correct value")
}
else {
XCTFail("Value isn't set")
}
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
}

You need to declare it 'lazy' like this:
lazy var sut : SecondViewController ...
That way it only gets created the first time it's accessed.
What's happening in your code is that every time you access a property of sut, you are creating a new instance of SecondViewController.
You create one instance with sut.view and a completely different one when you access sut.selectAudioButton. The second instance has not had its view loaded because you did not call .view on it!

Related

How can we write Unit Test Case for the function in swift ios

I want to write a unit test case for the method which is one of the delegate methods in the view controller. I created a unit test case class for the VC and am trying to write a unit test for the method.
Here is the method which is implemented in VC. How can we write Unit Test Case?
extension DownloadBaseViewController:EMPDecisionTreeCoordinatorDelegate {
func decisionEmptyTreeFeedbackButtonTapped() {
if let feedbackNavVc = storyboard?.instantiateViewController(identifier: "PremiumFeedbackNavViewController") as? PremiumCustomNavigationController {
if let feedbackVc = feedbackNavVc.children.first as? PremiumFeedbackViewController {
feedbackVc.id = self.fileDetails?.id
self.decesiontreeCoordinator!.rootViewController.present(feedbackNavVc, animated: true, completion: nil)
}
}
}
}
Created a unit test class for VC and tried not able to write it properly followed few tutorials not found for delegate method.
import XCTest
class DownloadBaseViewControllerTests: XCTestCase {
var downloadBaseViewController: DownloadBaseViewController!
func testDecisionEmptyTreeFeedbackButtonTapped() throws {
let feedbackVCNavigation = downloadBaseViewController.decisionEmptyTreeFeedbackButtonTapped
XCTAssertNotNil(feedbackVCNavigation, "Download base view controller contains feedback view controller and succesfully able to navigate")
///Test case Build succeded but this is not the way to test it properly need heads up on this.
}
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
}
Refactor the DownloadBaseViewController in your app so you can mock the dependency:
extension DownloadBaseViewController:EMPDecisionTreeCoordinatorDelegate {
// Add this variable in DownloadBaseViewController
lazy var presentingController: ViewControllerPresenting? = self.decesiontreeCoordinator?.rootViewController
func decisionEmptyTreeFeedbackButtonTapped() {
if let feedbackNavVc = storyboard?.instantiateViewController(identifier: "PremiumFeedbackNavViewController") as? PremiumCustomNavigationController {
if let feedbackVc = feedbackNavVc.children.first as? PremiumFeedbackViewController {
feedbackVc.id = self.fileDetails?.id
self.presentingController?.present(feedbackNavVc, animated: true, completion: nil)
}
}
}
}
// You need this to mock the foreign dependency on UIViewController
protocol ViewControllerPresenting: AnyObject {
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)?)
}
extension UIViewController: ViewControllerPresenting {}
In the tests you inject a Spy object that will help you validate the correct behaviour:
final class UIViewControllerSpy: ViewControllerPresenting {
var viewControllerToPresent: UIViewController!
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
self.viewControllerToPresent = viewControllerToPresent
}
}
class DownloadBaseViewControllerTests: XCTestCase {
var downloadBaseViewController: DownloadBaseViewController! = DownloadBaseViewController()
func testDecisionEmptyTreeFeedbackButtonTapped() throws {
// Given
let spyController = UIViewControllerSpy()
downloadBaseViewController.presentingController = spyController
// When
downloadBaseViewController.decisionEmptyTreeFeedbackButtonTapped()
// Then
let presentedController = spyController.viewControllerToPresent as? PremiumFeedbackViewController
XCTAssertNotNil(presentedController, "Download base view controller contains feedback view controller and succesfully able to navigate")
}
}

Swift: Error when trying to access a class method through a shared instance

class MoviesViewController5: UIViewController {
static let sharedInstance: MoviesViewController5 = MoviesViewController5()
func fillPage2(menuId menuId:String) {
// ...
}
}
When I try to access fillPage2 like this:
class TabsBarController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tab4ViewController = UIStoryboard(name: "Main", bundle: Bundle.main).instantiateViewController(identifier: "MoviesViewController5") as! MoviesViewController5
// Add each ViewController to your viewControllers array
// This throws an error
tab4ViewController.sharedInstance.fillPage2(menuId: "2")
// Static member 'sharedInstance' cannot be used on instance of type 'MoviesViewController5'
}
}
This is the error:
Static member 'sharedInstance' cannot be used on instance of type
'MoviesViewController5'
tab4ViewController will actually represent the view. So I need to call the fillPage2 method on it.
Any idea what I'm doing wrong?

instantiated class is nil when called inside viewdidload()

I am trying to learn the VIPER architecture model and one thing I can't figure out is when I do the following:
Instantiate promotionPresenter class
Instantiate promotionsViewController
assign promotionsViewController.presenter = (instantiated promotionPresenter class from step 1)
try to access the instantiated presenter class from inside viewdidload() function within promotionviewController class.
presenter is nil.
Why is presenter nil? I already instantiated it.
import UIKit
/*
* The Router responsible for navigation between modules.
*/
class PromotionsWireframe : PromotionsWireframeInput {
// Reference to the ViewController (weak to avoid retain cycle).
var promotionsViewController: PromotionsViewController!
var promotionsPresenter: PromotionsPresenter!
var rootWireframe: RootWireframe!
init() {
let promotionsInteractor = PromotionsInteractor()
// Presenter is instantiated
promotionsPresenter = PromotionsPresenter()
promotionsPresenter.interactor = promotionsInteractor
promotionsPresenter.wireframe = self
promotionsInteractor.output = promotionsPresenter
}
func presentPromotionsIntefaceFromWindow(_ window: UIWindow) {
//view controller is instantiated
promotionsViewController = promotionsViewControllerFromStoryboard()
//presenter of view controller is assigned to instantiaed class
promotionsViewController.presenter = promotionsPresenter
promotionsPresenter.view = promotionsViewController
}
private func promotionsViewControllerFromStoryboard() -> PromotionsViewController {
let storyboard = UIStoryboard(name: "PromotionsStoryboard", bundle: nil )
let viewController = storyboard.instantiateViewController(withIdentifier: "promotionsViewController") as! PromotionsViewController
return viewController
}
}
import UIKit
class PromotionsViewController : UIViewController, PromotionsViewInterface {
// Reference to the Presenter's interface.
var presenter: PromotionsModuleInterface!
var promotions: [Promotion]!
/*
* Once the view is loaded, it sends a command
* to the presenter asking it to update the UI.
*/
override func viewDidLoad() {
super.viewDidLoad()
// getting error because presenter is unwrapped as nil
self.presenter.updateView()
}
func showPromotionsData(_ promotions: [Promotion]) {
// need to implement
}
}
import Foundation
class PromotionsPresenter : PromotionsModuleInterface, PromotionsInteractorOutput {
// Reference to the View (weak to avoid retain cycle).
var view: PromotionsViewInterface!
// Reference to the Interactor's interface.
var interactor: PromotionsInteractorInput!
var wireframe: PromotionsWireframe!
func updateView() {
self.interactor.fetchLocalPromotions()
}
func PromotionsFetched(_promotions: [Promotion]) {
// need to implement
}
}
I suggest you take this boilerplate (https://github.com/CheesecakeLabs/Boilerplate_iOS_VIPER) and read this post (https://www.ckl.io/blog/best-practices-viper-architecture/) in order to learn not only how to correctly initialize your VIPER modules, but also how to automate VIPER files creation and initiazlization

XCTest: The internal class is not casted properly

XCTestCase subclass implementation is below:
import XCTest
#testable import AppName
class SomeTestClass: XCTestCase {
var viewController: AppName.ViewControllerName?
override func setUp() {
super.setUp()
viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("someIdentifier") as? AppName.ViewControllerName // Line 7
_ = viewController?.view
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testIfCaseInteractionLabelIsNotNil() {
XCTAssertNotNil(viewController?.someLabel)
}
}
In the code above the type of view controller object is "AppName.ViewControllerName" if I specify it as just ViewControllerName like below, the test fails because of casting in line 7
var viewController: ViewControllerName?
override func setUp() {
super.setUp()
viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("someIdentifier") as? ViewControllerName // Line 7
_ = viewController?.view
}
I tried making the view controller class public and using #testable annotation in swift2. It doesn't work.
I made "Enable Testability" build setting to YES.
The code works even if I don't use #testable import but in lot of blogs and tutorials, this results in error.
Any help would be appreciated! Thanks in advance

Instantiating UIViewController in a test

I'm working in Swift 2, and would like to test functions within my view controller. I've made a dependency injection-like service which looks like this:
extension UIViewController: {
func getDbService() -> IDbService {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
return DbService(context: context)
}
}
With this, I can set AppDelegate's context as a mocked one for test purposes. However, the problem arises when I try to instantiate a view controller. Here's the code:
class LoginViewController: UIViewController {
var token: String?
override func viewDidLoad() {
super.viewDidLoad()
let dbService = getDbService()
self.token = dbService.getToken()
//....do stuff with token
}
}
I instantiate the test like so:
class LoginViewControllerTests: XCTestCase {
func testTokenExists() {
let mockContext = MockContextUtils.getMockContext()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.managedObjectContext = mockContext
let sut = LoginViewController()
let _ = sut.view // Apparently this renders the view; I set a breakpoint, viewDidLoad is called
XCTAssertNotNil(sut.token) // FAILS, BECAUSE APPDELEGATE CALLS APP DATABASE, AND NOT MOCK.
}
}
The reason this simple test fails is because LoginViewController has no idea what app delegate is. Is there a way to introduce that in the initialization phase?
So it appears that initializing the view controller with its default controller does not grab the app delegate which we set up in the test. Instead, I've initialized the view controller through the storyboard via so:
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let sut = storyboard.instantiateInitialViewController() as? LoginViewController
XCTAssertNotNil(sut)
XCTAssertNotNil(sut.view)
XCTAssertNotNil(sut.token)
which solves the problem.

Resources