A framework for easily testing Push Notifications and Routing in XCUITests

Overview

Mussel 🦪 💪

A framework for easily testing Push Notifications, Universal Links and Routing in XCUITests.

Mussel Logo

As of Xcode 11.4, users are able to test Push Notifications via the simulator. Unfortunately, Apple has yet to introduce the ability to leverage this new method within the XCUITest Framework.

Testing Universal Links can also be an adventure, potentially accumulating lots of extra unwanted time in UI Tests, especially if your team wants to speed up your app's regression progress. Convential methods resorted to using iMessage or Contacts to open Universal Links which routed to a specific feature within an application.

Mussel introduces a quick and simple way to test Push Notifications and Universal Links which route to any specific features within your iOS app.

Let's Build some Mussel! 💪

How it works

Mussel Logo


  1. An Engineer triggers XCUITests in XCode manually or through your Continuous Integration platform of choice.
  2. Mussel Server boots up along with the iOS Simulator.
  3. A Test Case triggers a Push Notification or Universal Link Test Case.
  4. The Test Case sends a payload containing Push Notification or Universal Link data via POST Request.
  5. Server runs respective xcrun simctl command for Push Notifications or Universal Links.
  6. The command presents a Push Notification or launches a Universal Link within the iOS Simulator.

Installation

Mussel supports both Swift Package Manager and Cocoapods

Installing with Cocoapods

Add the Mussel pod to the project's UI Test Target in your podfile:

target 'ProjectUITests' do
    # Other UI Test pods....
    pod 'Mussel'
end

Installing with Swift Package Manager

Add Mussel dependency to your Package.swift

let package = Package(
    name: "MyPackage",
    ...
    dependencies: [
        .package(url: "https://github.com/UrbanCompass/Mussel.git", .upToNextMajor(from: "x.x.x")),
    ],
    targets: [
        .target(name: "MyTarget", dependencies: [.product(name: "Mussel", package: "Mussel")])
    ]
)

Usage

First, import the Mussel framework whenever you need to use it:

import Mussel

Push Notifications

Initialize your Mussel Tester of choice, we'll start with the MusselNotificationTester. Use your Target App's Bundle Id to ensure notifications are sent to the correct simulator.

let notificationTester = MusselNotificationTester(targetAppBundleId: "com.yourapp.bundleId")

Send a push notification with a simple message to your iOS Simulator:

notificationTester.triggerSimulatorNotification(withMessage: "Test Push Notification")

You can also send full APNS payloads for notifications with more complexity, supplying keys that are outside the aps payload. You can specify this payload as a Dictionary:

let testPayload = [
    "aps": [
        "alert": [
            "title": "Test title",
            "subtitle": "Test subtitle",
            "body": "Test body"
        ],
        "badge": 24,
        "sound": "default"
    ],
    "listingId": "12345"
]

Then call triggerSimulatorNotification with your respective dictionary-converted APNS payload.

notificationTester.triggerSimulatorNotification(withFullPayload: testPayload)

Universal Links

Initialize your MusselUniversalLinkTester using your Target App's Bundle Id to ensure notifications are sent to the correct simulator.

let universalLinkTester = MusselUniversalLinkTester(targetAppBundleId: "com.example.yourAppBundleId")

Trigger your iOS Simulator to open a Universal Link:

universalLinkTester.open("exampleapp://example/content?id=2")

Xcode build phases

In order for Mussel to work, the MusselServer must be running when tests are run. In CI it's recommended to download the MusselServer binary from the releases tab and ensure that is run before running your tests.

If you are using Bitrise you can also checkout the Mussel Bitrise Step which handles launching the server for you.

Cocoapods

When using Cocoapods and for local development, you can ensure the MusselServer is run before your UI tests are by adding a Run Script phase to your UI test scheme:

${PODS_ROOT}/Mussel/run_server.sh

Swift Package Manager

Since Swift Package Manager does not currently support run script phases for targets, you can get a similar experience by wrapping MusselServer as a build tool target and running it that way.

You can do this by:

  1. Create a BuildTools directory in the same parent directory as your Xcode project
  2. In the BuildTools directory create a Package.swift, which defines a target that can run MusselServer:
    import PackageDescription
    
    let package = Package(
        name: "BuildTools",
        platforms: [.macOS(.v10_13)],
        dependencies: [
            .package(url: "https://github.com/UrbanCompass/Mussel.git", from: "x.x.x"),
        ],
        targets: [.target(name: "BuildTools", path: "")]
    )
  3. Add an empty .swift file in the BuildTools directory.
  4. Add a new Run Script phase to your UI test scheme
    pushd BuildTools
    SDKROOT=macosx swift run -c release MusselServer > stdout 2>&1 &
    popd

NOTE: You may wish to check BuildTools/Package.swift into your source control so that the version used by your run-script phase is kept in version control. It is recommended to add the following to your .gitignore file: BuildTools/.build and BuildTools/.swiftpm.

Examples

Check out the example project in MusselExample

Here's a sample UI test that utilizes the Mussel framework for testing a Push Notification use case:

import Mussel
import XCTest

class ExamplePushNotificationTest: XCTestCase {
    let app = XCUIApplication()
    let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
    let notificationTester = MusselNotificationTester(targetAppBundleId: "com.yourapp.bundleId")

    func testSimulatorPush() {
        waitForElementToAppear(object: app.staticTexts["Mussel Push Notification Example"])
    
        // Launch springboard
        springboard.activate()

        // Trigger a push notification to the simulator
        notificationTester.triggerSimulatorNotification(withMessage: "Test Notification Message")

        // Tap the notification when it appears
        let springBoardNotification = springboard.otherElements["NotificationShortLookView"]
        waitForElementToAppear(object: springBoardNotification)
        springBoardNotification.tap()

        waitForElementToAppear(object: app.staticTexts["Mussel Push Notification Example"])
    }

    func waitForElementToAppear(object: Any) {
        let exists = NSPredicate(format: "exists == true")
        expectation(for: exists, evaluatedWith: object, handler: nil)
        waitForExpectations(timeout: 5, handler: nil)
    }
}

Here's a sample UI test that utilizes the Mussel framework for testing a Universal Link use case:

import Mussel
import XCTest

class ExampleUniversalLinkTest: XCTestCase {
    let app = XCUIApplication()
    let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
    let universalLinkTester = MusselUniversalLinkTester(targetAppBundleId: "com.example.yourAppBundleId")

    func testSimulatorPush() {
        waitForElementToAppear(object: app.staticTexts["Mussel Universal Link Example"])
    
        // Launch springboard
        springboard.activate()

        // Trigger a Universal Link to the simulator
        universalLinkTester.open("mussleSampleApp://example/content?id=2")

        waitForElementToAppear(object: app.staticTexts["Mussel Universal Link Example"])
    }

    func waitForElementToAppear(object: Any) {
        let exists = NSPredicate(format: "exists == true")
        expectation(for: exists, evaluatedWith: object, handler: nil)
        waitForExpectations(timeout: 5, handler: nil)
    }
}

Attribution

The original Mussel Icon can be found on clipartmax.com

Big thanks to Matt Stanford for finding an elegant and unprecedented way to test Push Notifications on the iOS Simulator with Pterodactyl

Contributing

Releasing

We are managing releases via Bitrise. This allows us to simplify the release process while getting rich release information in GitHub releases.

To create a release:

  • Do not create a release via GitHub, this is done by Bitrise.
  • Ensure that all the required commits are ready and merged into master.
  • Checkout to the master branch in your terminal and git pull to ensure you have the latest.
  • When you have determined the release version and have made sure youre on the latest master branch run:
    git tag -a VERSION
    git push origin VERSION
Comments
  • Any tips on running this on CI

    Any tips on running this on CI

    I'm having some issues running this on our Bitrise builds so I'm hoping someone may have run into this issue as well or has some insight that might get me on my way again..

    I've added some push notifications tests for our app, they run fine locally (very cool project so thanks!), but on bitrise builds the same tests fail with this less than helpful error message:

    Failed to get matching snapshot: Lost connection to the application (pid 25406). (Underlying Error: Couldn't communicate with a helper application. Try your operation again. If that fails, quit and relaunch the application and try again. The connection to service on pid 0 created from an endpoint was invalidated.)
    

    I assume the helper application referred to here is the XCTest helper app. But since all our other tests run fine I don't really understand why this particular test would fail.

    We use Xcode 12.4 both locally and on Bitrise.

    opened by tijs 3
  • ☠️ Tapping on the notification stops the UITests execution ☠️

    ☠️ Tapping on the notification stops the UITests execution ☠️

    Hello maintainers!

    I'm super excited about this library, it's a great work!

    We were giving it a try on our project but unfortunately we have discovered when the test taps on the notification, the testing session automatically halts given the simulator is triggering a new application launch.

    How to test it? (testing the tests)

    1. Include tearDown method on the unit test.
    2. Either check console or follow the lead using breakpoints.
    ///  pseudo
    app.waitForTapOnNotification(timeout: 5)
    
    /// This wait won't never been satisfied given tearDown is called due to the application halt.
    XCTAssertTrue(app.wait(for: .runningForeground, timeout: 10))
    

    You will check the tearDown gets executed right after tapping the notification, given the simulator is triggering a new PID for the app.

    Could you confirm you can reproduce on your side? I'd be awesome to get back from your side and tell me that I'm doing something wrongly.

    Cheers!

    opened by Ricowere 0
  • Followed the build phase section for xcode, but it is not spawning a Mussel server

    Followed the build phase section for xcode, but it is not spawning a Mussel server

    It seems that after the build phases, a executable of the server is not present.

    I am using swift-package-manager. Xcode version: Version 13.2 (13C90)

    System spec: macOS Big Sur Version 11.6.1 (20G224) Mac mini (M1, 2020) Chip Apple M1 Memory 16 GB

    https://github.com/UrbanCompass/Mussel#swift-package-manager

    opened by Billthekidz 0
  • unable to resolve product type 'com.apple.product-type.tool' in MusselServer

    unable to resolve product type 'com.apple.product-type.tool' in MusselServer

    I used SPM to install the mussel and followed the documentation written here.

    I was able to build the project in general but while running the deeplink tests I ran into the below error

    unable to resolve product tvpe 'com.apple.product-type.tool' for platform 'iphonesimulator' Screenshot 2021-12-22 at 13 31 37

    Additionally not sure if should target membership in the right pane ,if I do I see below error

    "No such module PackageDescription" Screenshot 2021-12-22 at 13 46 10

    I referred to the example but then its using Cocoapods. Is there anything I am doing it wrong or any extra steps you could provide to make this work.

    opened by sathyaNarayanQA 0
  • Support for

    Support for "Simulator device set"

    This is an attempt to address #24.

    Mussel client infers the device set based on the HOME environment variable, which contains a path to the application within the Simulator. If it contains the string "XCTestDevices", then we use the device set name "testing". However, this could be changed to specify the path of the device set instead of the magic name "testing" by truncating it.

    I changed the client to use a base class instead of protocol so that shared behavior can be written once. Identifying the current simulator ID and device set are in the base class. The base class implements the serverRequest(task:options:) method, and layers in these two items.

    On the server, if a request includes the new option, it's used in the simctl command line. I also changed this to fail early if the weak self has gone away, rather than return a success status.

    opened by RobinDaugherty 0
  • Fails when using Xcode parallelized test plan

    Fails when using Xcode parallelized test plan

    When Xcode has parallelized testing enabled, it creates a series of "Clone" devices in which to run the tests.

    MusselServer does well with multiple Simulator devices, but unfortunately in this case, the devices are not in the default device "set", so the server currently receives an error from simctl indicating that the device UDID is invalid.

    I was able to verify that injecting --set testing into the command line used by MusselServer fixes the issue for parallel test runs.

    Possible Solutions

    1. The test process determines that it's running in a parallelized mode and includes the device set in the HTTP payload it sends.

    There doesn't seem to be anything built in to Xcode to indicate that the test is running with parallelization. But it's possible for the Test Plan that has it enabled to also supply an environment variable or other startup options, and the UI Test process can use these to determine how to communicate with MusselServer.

    2. MusselServer is given a command-line option indicating which device set to interact with, then all commands issued by the process include that option.

    Since MusselServer is designed to run once, and listens on a single port, this seems like a bad idea—some Mussel clients might be running outside the parallelized environment, but they would have no way to differentiate, nor can another MusselServer instance be started.

    That could be worked around by using a different port for the "testing set" MusselServer process.

    But this is also investing in the assumption that there are only ever 2 device sets: default (unnamed) and "testing". This is probably not a correct assumption, as I can see some workflows that would take advantage of device sets for other reasons.

    opened by RobinDaugherty 0
  • Wait for server request to complete so UI test breakpoints work as expected

    Wait for server request to complete so UI test breakpoints work as expected

    The problem I experienced is:

    1. Set a breakpoint in your UI test right after a call to e.g. MusselUniversalLinkTester.open(link)
    2. UI test runs until breakpoint is reached
    3. Mussel Server never receives the request

    This is caused by the fact that URLRequest does work in another queue once the task is resumed, and by setting a breakpoint directly after resuming the task, the other queue never performs the work. The fix is to have Mussel wait until the URLRequest task completes before returning. I did this using a DispatchGroup.

    I also added some debugPrint calls so that when the request is complete, there's some evidence of it in the debug logs.

    opened by RobinDaugherty 0
Releases(1.0.0)
Owner
Compass
Compass Real Estate
Compass
iOS routing done right. Handles both URL recognition and controller displaying with parsed parameters. All in one line, controller stack preserved automatically!

Developed and Maintained by Ipodishima Founder & CTO at Wasappli Inc. (If you need to develop an app, get in touch with our team!) So what is this lib

null 589 Dec 24, 2022
URL routing library for iOS with a simple block-based API

JLRoutes What is it? JLRoutes is a URL routing library with a simple block-based API. It is designed to make it very easy to handle complex URL scheme

Joel Levin 5.6k Jan 6, 2023
RoutingKit - Routing library With Swift

RoutingKit Usage struct MessageBody: Body { typealias Response = String

HZ.Liu 3 Jan 8, 2022
An App-specific Simple Routing Library

TrieRouter An App-specific Simple Routing Library Usage let r = Router() r.addRoute("tbx://index") { _ in print("root") } r.addRoute("tbx://intTes

TBXark 2 Mar 3, 2022
Native, declarative routing for SwiftUI applications.

SwiftfulRouting ?? Native, declarative routing for SwiftUI applications Setup time: 1 minute Sample project: https://github.com/SwiftfulThinking/Swift

Nick Sarno 13 Dec 24, 2022
An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind

Composable Navigator An open source library for building deep-linkable SwiftUI applications with composition, testing and ergonomics in mind Vanilla S

Bahn-X 538 Dec 8, 2022
RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern

About Navigation concerns RxFlow aims to Installation The key principles How to use RxFlow Tools and dependencies GitHub Actions Frameworks Platform L

RxSwift Community 1.5k May 26, 2021
Interface-oriented router for discovering modules, and injecting dependencies with protocol in Objective-C and Swift.

ZIKRouter An interface-oriented router for managing modules and injecting dependencies with protocol. The view router can perform all navigation types

Zuik 631 Dec 26, 2022
Appz 📱 Launch external apps, and deeplink, with ease using Swift!

Appz ?? Deeplinking to external applications made easy Highlights Web Fallback Support: In case the app can't open the external application, it will f

Kitz 1.1k May 5, 2021
🎯Linker Lightweight way to handle internal and external deeplinks in Swift for iOS

Linker Lightweight way to handle internal and external deeplinks in Swift for iOS. Installation Dependency Managers CocoaPods CocoaPods is a dependenc

Maksim Kurpa 128 May 20, 2021
Monarch Router is a Declarative URL- and state-based router written in Swift.

Monarch Router is a declarative routing handler that is capable of managing complex View Controllers hierarchy transitions automatically, decoupling View Controllers from each other via Coordinator and Presenters. It fits right in with Redux style state flow and reactive frameworks.

Eliah Snakin 31 May 19, 2021
🍞 [Beta] A view controller that can unwind like presentation and navigation.

FluidPresentation - no more handling presented or pushed in view controller A view controller that supports the interactive dismissal by edge pan gest

Muukii 19 Dec 22, 2021
Eugene Kazaev 713 Dec 25, 2022
A bidirectional Vapor router with more type safety and less fuss.

vapor-routing A routing library for Vapor with a focus on type safety, composition, and URL generation. Motivation Getting started Documentation Licen

Point-Free 68 Jan 7, 2023
A bidirectional router with more type safety and less fuss.

swift-url-routing A bidirectional URL router with more type safety and less fuss. This library is built with Parsing. Motivation Getting started Docum

Point-Free 242 Jan 4, 2023
A simple, powerful and elegant implementation of the coordinator template in Swift for UIKit

A simple, powerful and elegant implementation of the coordinator template in Swift for UIKit Installation Swift Package Manager https://github.com/bar

Aleksei Artemev 22 Oct 16, 2022
Push Hero - pure Swift native macOS application to test push notifications

Dropdowns ❤️ Support my app ❤️ Push Hero - pure Swift native macOS application to test push notifications Quick Access - Organise files in the Mac men

Khoa 307 Oct 17, 2022
A simple, reliable and scalable delivery API for transactional push notifications for websites and applications

Catapush is a simple, reliable and scalable delivery API for transactional push notifications for websites and applications. Ideal for sending data-dr

Catapush 0 Dec 29, 2021
A SwiftUI iOS App and Vapor Server to send push notifications fueled by Siri Shortcuts.

Puffery An iOS App written in SwiftUI to send push notifications fueled by Siri Shortcuts. You can follow other's channels and directly receive update

Valentin Knabel 29 Oct 17, 2022
An iOS pre-permissions utility that lets developers ask users on their own dialog for calendar, contacts, location, photos, reminders, twitter, push notifications and more, before making the system-based permission request.

An iOS pre-permissions utility that lets developers ask users on their own dialog for calendar, contacts, location, photos, reminders, twitter, push notifications and more, before making the system-based permission request.

Joe L 420 Dec 22, 2022