Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Everything You (N)ever Wanted to Know about Testing View Controllers

11,753 views

Published on

3 easy tips for testing your view controller in Swift.

Published in: Technology

Everything You (N)ever Wanted to Know about Testing View Controllers

  1. 1. everythingyou(n)everwanted toknowabouttestingview controllers
  2. 2. unittestingisgreat…
  3. 3. …foryourmodellayer - Model layer is easy to test
 - Most examples of testing/TDD show model layer tests
  4. 4. canyoueventestview controllers? - But view controllers—ugh!
 - Can you even test them?
  5. 5. - Yes!
 - This is unfortunate misconception among many iOS devs
  6. 6. - Yes!
 - This is unfortunate misconception among many iOS devs
  7. 7. 1.Appmodule 2.Manuallifecycleevents 3.Storyboardaccessibility - Just a few simple tricks
  8. 8. - We’ll be testing my stealth mode app, BananaApp, made up of one view controller, BananaViewController
  9. 9. - We’ll be testing my stealth mode app, BananaApp, made up of one view controller, BananaViewController
  10. 10. 1.Appmodule 2.Manuallifecycleevents 3.Storyboardaccessibility - Public class - App defines a Swift module
  11. 11. - Defines module = YES -> classes in app exported as module
 - “Product module name” is the name of what you import in test file
  12. 12. // BananaViewController.swift public class BananaViewController: UIViewController { // ... } - To test classes, they must be exported in module, so they must be public
 - This goes for all classes, not just view controllers
  13. 13. // BananaViewControllerTests.swift import BananaApp import XCTest class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! // ... } - We import our defined module
 - Since class is public, we can reference it from imported module
  14. 14. // BananaViewControllerTests.swift import BananaApp import XCTest class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! // ... } - We import our defined module
 - Since class is public, we can reference it from imported module
  15. 15. // BananaViewControllerTests.swift import BananaApp import XCTest class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! // ... } - We import our defined module
 - Since class is public, we can reference it from imported module
  16. 16. 1.Appmodule 2.Manuallifecycleevents 3.Storyboardaccessibility - When app runs, view controller lifecycle methods are triggered automatically. - In tests, you’ll need to trigger methods like viewDidLoad: yourself.
  17. 17. // BananaViewController.swift import UIKit public class BananaViewController: UIViewController { // ... public override func viewDidLoad() { super.viewDidLoad() updateButtons() } private func updateButtons() { moreButton.enabled = bananaCount < 10 lessButton.enabled = bananaCount > 0 } } - BananaViewController overrides viewDidLoad to update its buttons
 - Let’s test this behavior
  18. 18. // BananaViewController.swift import UIKit public class BananaViewController: UIViewController { // ... public override func viewDidLoad() { super.viewDidLoad() updateButtons() } private func updateButtons() { moreButton.enabled = bananaCount < 10 lessButton.enabled = bananaCount > 0 } } - BananaViewController overrides viewDidLoad to update its buttons
 - Let’s test this behavior
  19. 19. // BananaViewController.swift import UIKit public class BananaViewController: UIViewController { // ... public override func viewDidLoad() { super.viewDidLoad() updateButtons() } private func updateButtons() { moreButton.enabled = bananaCount < 10 lessButton.enabled = bananaCount > 0 } } - BananaViewController overrides viewDidLoad to update its buttons
 - Let’s test this behavior
  20. 20. // BananaViewController.swift import UIKit public class BananaViewController: UIViewController { // ... public override func viewDidLoad() { super.viewDidLoad() updateButtons() } private func updateButtons() { moreButton.enabled = bananaCount < 10 lessButton.enabled = bananaCount > 0 } } - BananaViewController overrides viewDidLoad to update its buttons
 - Let’s test this behavior
  21. 21. // BananaViewControllerTests.swift class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! override func setUp() { viewController = BananaViewController() let _ = viewController.view } func testLessButtonIsDisabled() { XCTAssertFalse(viewController.lessButton.enabled) } } - Here we test the less button is disabled
 - But when we run the tests, they fail
 - Need to access view to trigger viewDidLoad
  22. 22. // BananaViewControllerTests.swift class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! override func setUp() { viewController = BananaViewController() let _ = viewController.view } func testLessButtonIsDisabled() { XCTAssertFalse(viewController.lessButton.enabled) } } - Here we test the less button is disabled
 - But when we run the tests, they fail
 - Need to access view to trigger viewDidLoad
  23. 23. // BananaViewControllerTests.swift class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! override func setUp() { viewController = BananaViewController() let _ = viewController.view } func testLessButtonIsDisabled() { XCTAssertFalse(viewController.lessButton.enabled) } } - Here we test the less button is disabled
 - But when we run the tests, they fail
 - Need to access view to trigger viewDidLoad
  24. 24. // BananaViewControllerTests.swift class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! override func setUp() { viewController = BananaViewController() let _ = viewController.view } func testLessButtonIsDisabled() { XCTAssertFalse(viewController.lessButton.enabled) } } XCTAssertFalse failed - Here we test the less button is disabled
 - But when we run the tests, they fail
 - Need to access view to trigger viewDidLoad
  25. 25. // BananaViewControllerTests.swift class BananaViewControllerTests: XCTestCase { var viewController: BananaViewController! override func setUp() { viewController = BananaViewController() let _ = viewController.view } func testLessButtonIsDisabled() { XCTAssertFalse(viewController.lessButton.enabled) } } - Here we test the less button is disabled
 - But when we run the tests, they fail
 - Need to access view to trigger viewDidLoad
  26. 26. 1.Appmodule 2.Manuallifecycleevents 3.Storyboardaccessibility - If your view controller’s interface is buried within a storyboard file, you’ll need to provide a way to access it
  27. 27. - You’ll need to give your view controller an ID
  28. 28. // BananaViewControllerTests.swift let storyboard = UIStoryboard(name: "Main", bundle: nil) viewController = storyboard.instantiateViewControllerWithIdentifier( "BananaViewControllerID") as BananaViewController - You can instantiate any view controller with an ID in your tests - If it’s the initial view controller in your storyboard, you don’t need an identifier
  29. 29. // BananaViewControllerTests.swift let storyboard = UIStoryboard(name: "Main", bundle: nil) viewController = storyboard.instantiateViewControllerWithIdentifier( "BananaViewControllerID") as BananaViewController viewController = storyboard.instantiateInitialViewController() as BananaViewController - You can instantiate any view controller with an ID in your tests - If it’s the initial view controller in your storyboard, you don’t need an identifier
  30. 30. public class BananaViewController: UIViewController { @IBOutlet public weak var countLabel: UILabel! @IBOutlet public weak var moreButton: UIButton! @IBOutlet public weak var lessButton: UIButton! // ... } - And remember, XIB and storyboard files set IBOutlet properties during -viewDidLoad
  31. 31. // BananaViewControllerTests.swift let storyboard = UIStoryboard(name: "Main", bundle: nil) viewController = storyboard.instantiateViewControllerWithIdentifier( "BananaViewControllerID") as BananaViewController let _ = viewController.view 
 XCTAssertFalse(viewController.lessButton.enabled) - So if you access an IBOutlet prior to triggering -viewDidLoad, you’ll hit an assert
  32. 32. Thread 1: EXC_BAD_INSTRUCTION // BananaViewControllerTests.swift let storyboard = UIStoryboard(name: "Main", bundle: nil) viewController = storyboard.instantiateViewControllerWithIdentifier( "BananaViewControllerID") as BananaViewController let _ = viewController.view 
 XCTAssertFalse(viewController.lessButton.enabled) - So if you access an IBOutlet prior to triggering -viewDidLoad, you’ll hit an assert
  33. 33. // BananaViewControllerTests.swift let storyboard = UIStoryboard(name: "Main", bundle: nil) viewController = storyboard.instantiateViewControllerWithIdentifier( "BananaViewControllerID") as BananaViewController let _ = viewController.view 
 XCTAssertFalse(viewController.lessButton.enabled) - So if you access an IBOutlet prior to triggering -viewDidLoad, you’ll hit an assert
  34. 34. 1.Appmodule 2.Manuallifecycleevents 3.Storyboardaccessibility - So remember
  35. 35. func testLessButtonAfterAddingBananaIsEnabled() { viewController.moreButton.sendActionsForControlEvents( UIControlEvents.TouchUpInside) XCTAssert(viewController.lessButton.enabled) } - With these in mind, we can write tests like this
 - Tap the button to add banana, then less button (to remove banana) is enabled
  36. 36. func testLessButtonAfterAddingBananaIsEnabled() { viewController.moreButton.sendActionsForControlEvents( UIControlEvents.TouchUpInside) XCTAssert(viewController.lessButton.enabled) } - With these in mind, we can write tests like this
 - Tap the button to add banana, then less button (to remove banana) is enabled
  37. 37. func testLessButtonAfterAddingBananaIsEnabled() { viewController.moreButton.sendActionsForControlEvents( UIControlEvents.TouchUpInside) XCTAssert(viewController.lessButton.enabled) } - With these in mind, we can write tests like this
 - Tap the button to add banana, then less button (to remove banana) is enabled
  38. 38. - But in the end, it’s not as easy as testing regular objects
 - So push as much logic into model layer as possible
  39. 39. - But in the end, it’s not as easy as testing regular objects
 - So push as much logic into model layer as possible
  40. 40. thinviewcontrollers! - Remember, thin view controllers!

×