Swift Unit Test- Wait for viewDidLoad - swift

I am new to Unit test in Swift. Now I wanted to test my viewController. In viewDidLoad I have an asynchronous call. So that if I want to test my Controller if the data got loaded correctly, the data didn't got loaded. I already read that I have to build in an XCTestExpectation.
So the information I have, I got from this question: XCTest Unit Test data response not set in test after viewDidLoad
The answer there is an example from which I don't know how to implement. My test class looks like this:
import XCTest
#testable import apirequest
class SearchedForViewControllerTests: XCTestCase {
var vc: SearchedForViewController!
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
vc = storyboard.instantiateViewController(withIdentifier: "SearchedForViewController") as! SearchedForViewController
vc.passedString = "garten"
let _ = vc.view
}
func testArticlesShwon() {
print(vc.tableView.numberOfRows(inSection: 0))
}
}
So if I look on my own code the part of viewDidLoad happens in
let _ = vc.view
If I want to build in a Expectation, I have to wait for this part. But the part is not a function. So I don't know how I could tell my Expectation to fulfill after loading.

This may be more of an opinion/design answer, but I would highly recommend testing your ViewController and your Model completely separately.
E.g. when you're testing your VC, manually set the data in your tests, then separately have tests to ensure your model and any networking/async calls are functioning properly.

Related

Could not load Main.Storyboard in XCUITest

I am try to make UITestCase for UIViewcontrollers, But when I load main storyboard in my QuizAppUITests, It could not identify from Bundle and it's gives below error
Could not find a storyboard named 'Main' in bundle NSBundle </Users/mac/Library/Developer/CoreSimulator/Devices/CC199C69-F398-4A7C-882E-BFD3E72B95D3/data/Containers/Bundle/Application/BCC3F88D-E08C-4491-8340-699EAF98AB28/QuizAppUITests-Runner.app> (loaded) (NSInvalidArgumentException)
I have added Main Storyboard in QuizAppUITests Target as well
And below is my code for test
import XCTest
#testable import QuizApp
class QuizViewControllerUITests: XCTestCase {
func makeSUT() -> QuizViewController {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: type(of: self)))
let sut = storyboard.instantiateViewController(withIdentifier: "QuizViewController") as! QuizViewController
_ = sut.view
return sut
}
func test_loadQuizViewController() {
let sut = makeSUT()
sut.headerQuestion = "Q1"
XCTAssertEqual(sut.headerQuestion, "Q1")
}
}
Is there any required to change in BuildSetting of QuizAppUITests Target?
None of this makes sense for a UI test. Instead, put your code into your unit test target, which is QuizAppTests.
My book iOS Unit Testing by Example has a chapter called "Load View Controllers". Looking there for what it says about storyboard-based view controllers:
Don't include the storyboard in your test target. Put it only in your app target.
Then you can load the storyboard with UIStoryboard(name: "Main", bundle: nil)
There's a new way to instantiate the view controller that doesn't require force-casting. Instead of instantiateViewController(withIdentifier:), use instantiateViewController(identifier:) and assign it to an explicitly typed variable.
Like this:
let sut: QuizViewController = storyboard.instantiateViewController(
identifier: "QuizViewController"
)
Since you use the name of the class as the identifier, we can even get rid of the string. This protects us from typos, at compile time:
let sut: QuizViewController = storyboard.instantiateViewController(
identifier: String(describing: QuizViewController.self)
)
Finally, instead of _ = sut.view, we can be more explicit about loading the view:
sut.loadViewIfNeeded()
This hooks up the outlet connections.
Again, all this belongs in your unit test target QuizAppTests, not your UI test target QuizAppUITests.
For your actual tests, don't just assign a property in your system under test and check that it was assigned. That doesn't prove anything. Instead, I'd focus on testing:
That outlets are not nil
That interacting with controls does what you want
That navigation works
That view appearance hasn't changed from an approved snapshot
This can all be done with TDD.

EXC_BAD_ACCESS on device, but works on a Simulator

I had written a function below which is inside Framework A:
open func invokeTracking(key: String, bundle: Bundle? = Bundle.main) {
let qos = DispatchQoS.QoSClass.default
DispatchQueue.global(qos: qos).async {
self.invokeAsyncTracking(key: String, bundle: bundle)
}
And I am calling this in an Xcode project in multiple places and also from other frameworks.
In Xcode project:
override func viewDidLoad() {
super.viewDidLoad()
HelperClass.invokeTracking(key: "Camino", bundle: Bundle? = Bundle.main)
}
Similarly calling it from Framework B
func callMainTracking() {
super.viewDidLoad()
HelperClass.invokeTracking(key: "Carthage", bundle: Bundle? = Bundle.main)
}
This is working fine and has worked for almost a year. Recently I added another additional default parameter to the invoke tracking method like this so as not to affect the existing method calls like below. And I am trying to set the value of this parameter of a global variable:
open func invokeTracking(key: String, bundle: Bundle? = Bundle.main, conventDict: [String: Any] = [:]) {
mainDict = conventDict
let qos = DispatchQoS.QoSClass.default
DispatchQueue.global(qos: qos).async {
self.invokeAsyncTracking(key: String, bundle: bundle)
}
The app is crashing in the main thread on the line mainDict = conventDict when trying to access that default parameter. If I do it inside the async its crashing in that async thread. But this crash is happening only when it's called from Framework B and not when its called from the Xcode project. Also this crash is happening only on the device.
I have tried the following to resolve this:
Tried to declare default parameter as optional and using if let to unwrap it.
Tried with different data types.
Tried by changing arguments order.
And the only way it does not crash is if I send a value for this default parameter from Framework B while calling this method.
func callMainTracking() {
super.viewDidLoad()
HelperClass.invokeTracking(key: "Carthage", bundle: Bundle? = Bundle.main, conventDict: ["test": "123"])
}
But this defeats the purpose of a usage of a default parameter. Im new to debugging Xcode crashes so any help would be appreciated.
I used the Instruments and tried to find a Zombie but there wasnt one. The app simply just crashed and no zombie process was detected.

instantiateViewController throws SIGABRT on Unit Test

I'm writing unit tests for a controller that has elements (like buttons) created in storyboard. I'm trying to instantiate the view controller in my Unit Test so I can access those elements and not have the app crash. However, the way that I am trying to instantiate my storyboard is causing Xcode to throw a SIGABRT error on the line that I am calling the instantiateViewController command.
This is the code that I am using to try and instantiate my storyboard in my unit test file:
func testAreaActionsViewController() {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: self.classForCoder))
let viewController = storyboard.instantiateViewController(withIdentifier: "AreaActionsViewController") as! AreaActionsViewController
// view.loadView()
//
// view.viewDidLoad()
}
Does anyone have an idea as to why it is throwing SIGABRT? I verified in my storyboard that the Identifier is AreaActionsViewController, and also set the target in my Main.storyboard to include my testing target.
Sorry for late but I had same issue and I solved removing this:
#testable import ProjectName

All ViewController Views are Nil in Unit Testing?

Sorry if this is a beginner question but I'm relatively new to Unit testing and didn't see this asked anywhere.
When I start my unit tests in Swift, I setUp my tests by instantiating my viewController.
My code is set up using MVVM (Model - View - ViewModel). So when I test some of my viewModel methods, they will update the Views (in the ViewController) in the UI. The problem is, Xcode keeps crashing and says that the views in the ViewController are nil? How do I prevent these views from being nil? Am I doing something wrong? How do I instantiate the views within the viewController? I thought this would be automatic.
class WeirdFaceTests: XCTestCase {
var viewController: ViewController?
var tattooModel: ARModel?
var tattooViewModel: ARViewModel?
var mainUIModel: MainUIModel?
var mainUIViewModel: MainUIViewModel?
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
self.viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "PrimaryViewController") as! ViewController
self.tattooModel = ARModel(imageName: "blank", tattooType: .new)
self.tattooViewModel = ARViewModel(tattooModel: tattooModel!, delegate: viewController!)
self.mainUIModel = MainUIModel()
self.mainUIViewModel = MainUIViewModel(model: mainUIModel!, delegate: viewController!)
}
Turns out I was missing the self.viewController?.loadView() call in the setUp() method.
The correct code that works without nil views is as follows:
class WeirdFaceTests: XCTestCase {
var viewController: ViewController?
var tattooModel: ARModel?
var tattooViewModel: ARViewModel?
var mainUIModel: MainUIModel?
var mainUIViewModel: MainUIViewModel?
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
super.setUp()
self.viewController = UIStoryboard(name: "Main", bundle:nil).instantiateViewController(withIdentifier: "PrimaryViewController") as! ViewController
self.viewController?.loadView()

What is the use of beforeEach method in Kiwi/Quick test frameworks?

Why is beforeEach used in test frameworks like Kiwi and Quick?
beforeEach {
let storyboard = UIStoryboard(name: "Main",
bundle: NSBundle(forClass: self.dynamicType))
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
viewController = navigationController.topViewController as ViewController
UIApplication.sharedApplication().keyWindow!.rootViewController = navigationController
let _ = navigationController.view
let _ = viewController.view
}
Example code taken from: https://www.natashatherobot.com/unit-testing-in-swift-a-quick-look-at-quick/
Is the equivalent of Apple's Test Framework setUp method. It is executed before every test, to set up components and mocks that will be used in every test.
According to Kiwi's wiki:
beforeEach(aBlock) is run before every it block in all enclosed contexts. Code that actually sets up the particular context should go here.
It's helpful to avoid code repetition and that your tests have a standard setup among them.
In the piece of code that you shared, the beforeEach method is setting up a UINavigationController with a rootViewController extracted from the storyboard. It is also loading the view so the tests can access components such as outlets.