Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing presented view controller and keyboard in iOS

I'm trying to write unit tests to ensure that the keyboard and presented view controllers are triggered properly, but I'm getting the strange behaviour I don't understand that I believe is related to how UIWindow works. I'm using Quick and Nimble, but I've tested with vanilla XCTest and get the same issues.

My Code:

import Quick
import Nimble

class TestSpec: QuickSpec {
    override func spec() {

        let sut = UIViewController()

        // The window is nil when this test is run in isolation
        UIApplication.shared.keyWindow?.rootViewController = sut

        // This does not work either
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = sut
        window.makeKeyAndVisible()

        describe("ViewController") {

            it("presents a UIAlertController") {
                let alert = UIAlertController(title: "Test", message: "This is a test", preferredStyle: .alert)
                let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
                alert.addAction(okAction)

                sut.present(alert, animated: true, completion: nil)
                expect(sut.presentedViewController).toEventually(beAnInstanceOf(UIAlertController.self))
            }
        }
    }
}

At first, I wasn't putting the view controller in a window, which prevented it from presenting other view controllers. Now I'm trying to put in in a window, but that isn't working either. When this test is run in isolation the window is always nil. When I run it with a bunch of other tests the window is not nil sometimes, but tests still fail. There was a brief period where the tests did pass, but I can't replicate it anymore for some reason.

Any ideas what's up?

like image 921
Erik Avatar asked Mar 18 '17 08:03

Erik


People also ask

How do I unit test a view controller?

To make your Unit Test load the UIViewController that has an interface XIB file is even easier. Create an instance on UIViewController that uses XIB file. Make sure though that the view controller and the XIB files have the same names. Call the loadViewIfNeeded() method to make the viewDidLoad() execute.

What is unit testing in IOS?

A unit test is a function you write that tests something about your app. A good unit test is small. It tests just one thing in isolation. For example, if your app adds up the total amount of time your user spent doing something, you might write a test to check if this total is correct.

What is a view controller in IOS?

A view controller acts as an intermediary between the views it manages and the data of your app. The methods and properties of the UIViewController class let you manage the visual presentation of your app. When you subclass UIViewController , you add any variables you need to manage your data in your subclass.

Do we write unit test for controller?

Unit tests of controller logic. Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. When unit testing controller logic, only the contents of a single action are tested, not the behavior of its dependencies or of the framework itself.


2 Answers

Thanks for opening this question, I wouldn't have thought about the UIWindow issue if I hadn't bumped into this.

After fiddling a bit with your code I was able to make my test pass by doing this:

beforeEach {
  let window = UIWindow(frame: UIScreen.main.bounds)
  window.makeKeyAndVisible()
  window.rootViewController = sut
  _ = sut.view
}

Two things to note:

let window = UIWindow(frame: UIScreen.main.bounds)

In my tests the value of UIApplication.shared.keyWindow is nil. I'm not sure if that would be the case in yours as well, I prevent the unit tests from loading the UIAppDelegate which may or may not have something to do with it.

Also note that the window instance is not retained by the spec class. I believe this is unnecessary because when it's made key window the UIApplication instance holds it, and UIApplication won't get released.

_ = sut.view

This line is what does the trick. Trying to access the view of the view controller results in it being actually added to the view hierarchy (more info here, which solves the warning one would otherwise get:

Warning: Attempt to present <BarViewController: 0x7f87a9c5def0>
on <FooViewController: 0x7f87a9d5e320> whose view is not in the window 
hierarchy!

Hope this helps ;)

like image 168
mokagio Avatar answered Sep 20 '22 08:09

mokagio


I've tried the approach my mokagio successfully in the past, so I'm happy to see that presented here as well. It seems to create problems with the actual running of the test suite sometimes, so I shied away from it recently. Perhaps that's a change in how the iOS test runner works.

I've ended up a snippet that just stubs out the presentation, so I can test the interaction without worrying about screens actually needing to be presented, eg.

class MockPresentingViewController: UIViewController {
  var presentViewControllerTarget: UIViewController?

  override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) {
    presentViewControllerTarget = viewControllerToPresent
  }
}

And then in my tests, I can:

// action
container.present(viewController, animated: true, completion: nil)

// expectation
expect(host.presentViewControllerTarget).to(...)
like image 38
Stew Avatar answered Sep 17 '22 08:09

Stew