GithubHelp home page GithubHelp logo

jwfriese / fleet Goto Github PK

View Code? Open in Web Editor NEW
24.0 6.0 6.0 1.02 MB

Storyboard injection and helpful extensions to enable fast, consistent UIKit testing in iOS

License: Apache License 2.0

Swift 92.84% Objective-C 2.79% Ruby 0.66% Shell 0.57% Go 3.14%

fleet's Introduction

Fleet Build Status

Fleet is a UIKit-focused testing framework intended for use on iOS and tvOS projects written in Swift.

Installation

See the Installation section for details about installing Fleet in your iOS or tvOS project.

Features

Storyboard injection and mocking

Fleet allows test code to inject into and pull mocks from storyboards. This gives you the best of both worlds -- ability to test your view controllers and their interactions with each other exactly as they are in production code without compromising the ability to isolate any particular view controller for unit testing purposes.

Read more about using Fleet's storyboard features in the Documentation section.

Interaction with UIKit elements

Fleet extends UIKit classes in order to make it easier to test your code's interactions with UIKit. Following are summaries of the major features for each extended class. The Documentation folder contains more information about these features, as well as sample code showing you how they are intended to be used.

  • UIViewController - Makes the UI run-loop behave more consistently to allow specs to test view controller presentation and dismissal with less effort.
  • UITableView - Provides methods mimicking user actions on table views, ensuring all appropriate delegate and data source callbacks are run.
  • UINavigationController - Allows specs to test pushing and popping on the navigation stack without waiting for the UI run-loop.
  • UIButton - Provides convenience methods for interacting with buttons in test.
  • UIBarButtonItem - Provides convenience methods similar to those provided for UIButton.
  • UIAlertController - Allows specs to simply tap on alert actions in order to test their behavior.
  • UITabBarController - Provides convenience methods for selecting tabs.
  • UINavigationBar - Tap on items in a navigation bar.
  • UITextField - Provides convenience methods for entering text. (iOS-only)
  • UISwitch - Provides convenience methods for interacting with switches in test. (iOS-only)
  • UITextView - Provides convenience methods for entering text into a text view. (iOS-only)
  • UIToolbar - Tap on items in a toolbar. (iOS-only)

Setup for view controller tests

Fleet provides another method of help in setting up view controller alongside storyboard injection and mocking. It makes it easy to set your view controllers up in a proper key application window by providing the following methods:

// Takes a `UIViewController`,  makes it the test app key window's root, and kicks off its lifecycle.
Fleet.setAsAppRootWindow(_:)

// Takes a `UIViewController`,  makes it the root of a navigation stack, kicks off the lifecycle, and
// returns the navigation controller that hosts that view controller.
Fleet.setInAppWindowRootNavigation(_:)

Fleet highly recommends that all view controller tests run inside the test host's key window using one of the above methods. For more discussion on why, go to the relevant section in the FAQ.

FAQ

Examples

To get you started with Fleet, we've created a real, working project that is fully test-driven with the help of Fleet, and which contains many examples of basic tests that you'll likely write in any Swift iOS application.

For more complicated, larger-scale projects that use Fleet, check out the following projects:

FrequentFlyer

fleet's People

Contributors

dependabot[bot] avatar farshadtx avatar jwfriese avatar wojciechczerski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

fleet's Issues

Warnings

We get the following warnings when building with Fleet:

[20:07:11]: ▸ /Users/pivotal/Library/Developer/Xcode/DerivedData/BKO-ftkizwpavnneyraiwmzhzmnoovur/Logs/Test/62028079-6A4A-4599-A9FC-975F5D70CD1F/Session-2016-06-23_20:07:11-dlabNt.log
[20:07:12]: ▸ ⚠️  /Users/pivotal/workspace/bk-native-app/Pods/Fleet/Fleet/CoreExtensions/UIAlertAction+Fleet.swift:32:41: no method declared with Objective-C selector 'setHandler:'
[20:07:12]: ▸ let originalSelector = Selector("setHandler:")
[20:07:12]: ▸                                         ^
[20:07:12]: ▸ ⚠️  /Users/pivotal/workspace/bk-native-app/Pods/Fleet/Fleet/CoreExtensions/Storyboard/UIStoryboard+Fleet.swift:60:41: no method declared with Objective-C selector 'initWithBundle:storyboardFileName:identifierToNibNameMap:identifierToExternalStoryboardReferenceMap:designatedEntryPointIdentifier:'
[20:07:12]: ▸ let originalSelector = Selector("initWithBundle:storyboardFileName:identifierToNibNameMap:identifierToExternalStoryboardReferenceMap:designatedEntryPointIdentifier:")
[20:07:12]: ▸                                         ^
[20:07:12]: ▸ ⚠️  /Users/pivotal/workspace/bk-native-app/Pods/Fleet/Fleet/CoreExtensions/Storyboard/UIStoryboard+Fleet.swift:107:41: no method declared with Objective-C selector 'instantiateViewControllerReferencedByPlaceholderWithIdentifier:'
[20:07:12]: ▸ let originalSelector = Selector("instantiateViewControllerReferencedByPlaceholderWithIdentifier:")

UIAlertController Failure on version 2.2.0

Hey Jared,

We recently updated our Carthage Repo add decided to go with the latest version of Fleet(ver 1.0.2 to ver 2.2.1).

Unit tests will crash because of a casting on UIViewController which is due to tapping on an action on Alert Controller.

We downgraded to 2.1.0 and it looks it is fine. I will try to provide more info about it later. (just added this here so we can keep tracking it)

Thanks

Unable to bind fake view controllers in test

We are attempting to use Fleet's bindViewController to mock out view controllers that we segue to in our tests. Unfortunately, the assertions that we make to prove that we have performed the segue do not seem to work:

class PaymentsViewControllerSpec: SwinjectSpec {
    override func spec() {
        describe("PaymentsViewController") {
            var subject: PaymentsViewController!
            var creditCardViewController: CreditCardViewController!

            beforeEach {
                let paymentStoryboard = UIStoryboard(name: "Payment", bundle: NSBundle.mainBundle())
                creditCardViewController = CreditCardViewController()
                try! paymentStoryboard.bindViewController(creditCardViewController, toIdentifier: "CreditCardViewController")

                subject = self.instantiateController("PaymentsViewController", storyboardName: "Payment") as! PaymentsViewController

                let window = UIWindow()
                window.rootViewController = subject
                window.makeKeyAndVisible()
                _ = subject.view
            }

            it("segues to the add credit card page") {
                didSelectRowAt(creditCards.count + 1)
                let topmostViewController = Fleet.getCurrentScreen()?.topmostPresentedViewController

                // FAILS: view controllers have different memory addresses
                expect(topmostViewController).to(beIdenticalTo(creditCardViewController)) 

                // FAILS: presented view controller is nil
                expect(subject.presentedViewController).to(beIdenticalTo(creditCardViewController))
            }
        }
    }
}

Here is approximately what our implementation looks like:

class PaymentsViewController: UIViewController, UITableViewDelegate {
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        performSegueWithIdentifier("showCreditCard", sender: nil)
    }
}

We are doing this on a view controller that has a direct segue from PaymentsViewController to the CreditCardViewController.

Maybe a sample project would be helpful to show how this is done?

selectRow helper not working properly with RxSwift/RxCocoa

When using selectRow with RxSwift/RxCocoa, selectRow acts like a no-op, apparently because RxSwift/RxCocoa does not implement willSelectRow and the default delegate behavior for that method is (oddly, at least during our unit test) to return nil.

Tapping on UIBarButtonItems would be nice

What are the chances of getting the ability to tap on UIBarButtonItems? Perhaps something like this:

extension UIBarButtonItem {
    func tap() {
        if self.target!.enabled! {
            self.target!.performSelector(self.action, withObject:self)
        }
    }
}

Or you could do something more extravagant like PivotalCoreKit

Segues in NavigationController viewDidLoad are not captured by Fleet

This is more of a potential bug. I would of investigated it a lot more, but I don't have much experience with swizzling. So, we are probably doing this completely wrong on our project, but we have code in our NavigationController (that is our initialViewController for our main Storyboard). Specifically it starts off a network call (unrelated, but it does some work) and without waiting for the response from that call, it segues to another viewController.

Test Code:

beforeEach {
    subject = mainStoryboard.instantiateViewControllerWithIdentifier("NavigationController") as! NavigationController
    subject.menuBroker = fakeMenuBroker

    let window = UIWindow()
    window.rootViewController = subject
    window.makeKeyAndVisible()
}

.......

describe("#viewDidLoad") {
    it("begins the fetch for the store menu and store") {
        expect(fakeMenuBroker.updateStoreMenuAndStoreByLocationCallCount).to(equal(1))
    }

    it("saves the store menu and store fetch future") {
        expect(subject.promiseToSetMenuAndStore).to(beIdenticalTo(menuBrokerPromise.future))
    }

    it("segues to the loading view") {
        let expectedLoadingViewController = Fleet.getCurrentScreen()?.topmostPresentedViewController as! LoadingViewController
        expect(expectedLoadingViewController).to(beIdenticalTo(loadingViewController))
    }
}

Controller Code:

override func viewDidLoad() {
    super.viewDidLoad()

    promiseToSetMenuAndStore = menuBroker.updateStoreMenuAndStoreByLocation()
    performSegueWithIdentifier(Segues.loading, sender: nil)
}

The window.makeKeyAndVisible() triggers the viewDidLoad and viewWillAppear on the viewController. Here's what happens in test:

  1. Before window.makeKeyAndVisible: Fleet.getCurrentScreen()?.topMostPresentedViewController points to some random UIViewController, no idea what it is....
  2. After window.makeKeyAndVisible: Fleet.getCurrentScreen()?.topMostPresentedViewController points to the NavigationController, even though it performs a segue in the viewDidLoad

My current workaround is to not use Fleet for this test, and instead global extend the controller and override the performWithSegue to update a global variable that can be checked in test....

Tapping on a AlertController Action does not dismiss the AlertController automatically

Using the .tapAlertActionWithTitle method does correctly call the callback I have set for the action being tapped, however it does not dismiss the UIAlertController automatically afterwards. I assumed it would be dismissed since in my production code callback for that action I do not manually dismiss the alert controller and when I run the app, the alert controller is dismissed when I tap that action.

What currently happens in test:

  1. Execute code to cause AlertController to appear
    -- Fleet.currentScreen()?.presentedAlert OR Fleet.currentScreen()?.topMostPresentedViewController both point at the UIAlertController correctly.

  2. Use .tapAlertActionWithTitle on a title that exists
    -- Fleet.currentScreen()?.presentedAlert OR Fleet.currentScreen()?.topMostPresentedViewController both STILL points at the same UIAlertController.

I expected this:

  • Fleet.currentScreen()?.presentedAlert to be nil
  • Fleet.currentScreen()?.topMostPresentedViewController to be the view controller that presented the alert

I am currently adding dismissViewControllerAnimated in the action callback (in the production code) to have this test work the way I expect.

Possible crash when calling UITextField.backspace()

The implementation of the backspace() method contains a branch that calls the shouldChangeCharactersIn(...) on a delegate without checking beforehand, whether this optional method is implemented).

if existingText == "" {
    if let delegate = delegate {
        // this still gets called in this case
        let _ = delegate.textField!(self, shouldChangeCharactersIn: NSMakeRange(0, 0), replacementString: "")
    }

    return
}

Calling this method should be conditional and dependent on whether the delegate implements it. It has been done for other calls to shouldChangeCharactersIn(...), for example:

let doesImplementShouldChangeText = delegate.responds(to: #selector(UITextFieldDelegate.textField(_:shouldChangeCharactersIn:replacementString:)))
var doesAllowTextChange = true
if doesImplementShouldChangeText {
    doesAllowTextChange = delegate.textField!(self, shouldChangeCharactersIn: NSMakeRange(location, backspaceAmount), replacementString: "")
}

Would be nice to have UITextField.enterText

It would be awesome if we could have the ability to enter text and trigger an editing changed control event. Instead of:

subject.passwordField.text = "some-password"
subject.passwordField.sendActionsForControlEvents(UIControlEvents.EditingChanged)

I am imagining:

subject.passwordField.enterText("some-password")

Perhaps it could also trigger EditingDidBegin and EditingDidEnd (or AllEditingEvents).

What do you think about this idea?

Support for UISwitch

It would be great to have the capability to do the equivalent of "tap" on a UISwitch like we can do on a UIButton. Unfortunately, sendActions(.touchUpInside) doesn't seem to do anything as I would have expected it to.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.