How to Create Public Extensions, in a Shared Framework, for Xctest

How do you create public extensions, in a shared framework, for XCTest?

I've created an xcworkspace here (https://github.com/dtweston/TestFrameworkSample) that demonstrates a solution to your issue. There are two projects in this workspace:

  1. SampleApp project with an iOS app target and a unit test target.
  2. SharedTestFramework project that imports XCTest and declares the single extension you put in your question.

The SampleAppTests target links to the SharedTestFramework to be able to use the extension it defines. The single test file imports the SharedTestFramework.

With those steps, I also encounter the Cannot load underlying module for 'XCTest' when building the SharedTestFramework.

The fix for that is to update the Framework Search Paths to include "$(PLATFORM_DIR)/Developer/Library/Frameworks". Now the SharedTestFramework compiles correctly, and as you can see in the workspace I uploaded, the SampleAppTests target is able to use it successfully.

Old and busted answer

Are you building a separate framework that is designed to be imported into test targets? If that's the case then I think you just need to reference XCTest.framework from this custom framework you're building.

On the other hand, if you're trying to add this extension to a framework that is used by your app target, that seems like a bad idea, because it would mean linking XCTest.framework to the binary that goes to the store and runs on people's devices.

I'm not sure if that's possible. I'm more confident that it's not a scenario Apple expects or supports.

Creating a framework that uses XCTest

You have to add additional Framework Search Path in the Build Settings:

$(PLATFORM_DIR)/Developer/Library/Frameworks

Writing a unit-test for a class extension in Swift

As @dan alluded to, you need to call it from an instance of the UIViewController. Usually you don't want to instantiate framework objects in your tests if you can avoid it, so here are some options to avoid that:

  1. Make presentAlert static so that you can just UIViewController.presentAlert
  2. Make presentAlert a free function (don't put it in the extension)
  3. Extend a protocol instead – I think this is the cleanest option

protocol Presentable {}

extension Presentable {
func presentAlert(title: String, message : String) { /* ... */ }
}

Then, whenever you need it you can extension UIViewController: Presentable {}. And in your tests, you can just use a dummy class. The added benefit with this approach is that you can reuse that function on any type if needed, without globally exposing it when you don't.

Addendum

When we extend the protocol we are saying "anything that implements this protocol will get this method for free." The trick here is that this protocol is empty and, therefore, very easy to "implement."

extension YourViewController: Presentable {}

How To Link XCTest Dependency To Production / Main Target?

Auxiliary information on XCTest itself is sparse and hard to find, I was also chasing down the same functionality and finally managed to get it working.

I am on XCode 10.1 and running on a real iPhone with iOS 11. I am certain the general technique will work for other versions, but probably will require a few tweaks.

The general steps are described in this stackoverflow answer, but required several additional steps and tweaks to work for me on a real iPhone:

Is it possible to run XCTest tests in an iOS app?

Follow the steps in the above link. The below steps are deviations from those instructions that were required for me.

  1. Copy in the XCTest framework as described in the above link. NOTE: Use the framework for the iPhone.OS platform and not the simulator as it describes. You can find this framework file inside the actual XCode Application package on your mac. (Right click, "Show Package Contents", then look in ./Contents/Developer/Platforms/iPhoneOS.platform

  2. Disable bitcode in your app target. This solves a linker error. Here is an example of enabling it: how to ENABLE_BITCODE in xcode 7?

  3. When dragging the XCTest.framework file to the linked binaries in your target, ensure that you also drag it to the "Embedded Binaries" which is directly above the "Linked Frameworks and Libraries" option. If you don't do this you will get a runtime error.

add framework to embedded binaries


  1. The ViewController code to start the tests is slightly different in new Swift, here is what I am using:

import UIKit
import XCTest

class ViewController: UIViewController {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("running tests!")
let suite = XCTestSuite.default;
for test in suite.tests {
test.run()
}
}

}

That should be it! When I run the above app, then touch the screen, all of the tests from my UITesting target run flawlessly and pass!

How to trigger another method or task based on testcase failure or skipped or pass in xctest framework

In XCode 9 Apple changes its previous XCTestObservation to XCTestObservationCenter. You will find the reference here

To do something based on the TC pass or fail you need to register the testObserver and add it to your test.

Here is the complete implementation for this.

Step 1: Create a class called observer.

 import Foundation
import XCTest

class Observer: NSObject, XCTestObservation {
var failedTC = 0

func testBundleWillStart(_ testBundle: Bundle) {
print("Test bundle started")
}

func testBundleDidFinish(_ testBundle: Bundle) {
print("Test bundle Finished")
}

func testSuiteWillStart(_ testSuite: XCTestSuite) {
print("Test suite started")
}

func testSuiteDidFinish(_ testSuite: XCTestSuite) {
print("Test suite ended")
}

func testSuite(_ testSuite: XCTestSuite, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) {
print("Test suite failed, information: " + description)
}

func testCaseWillStart(_ testCase: XCTestCase) {
print("Test case started")
}

func testCaseDidFinish(_ testCase: XCTestCase) {
print("Test case finished")
}

func testCase(_ testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) {
print("Test case failed. Message: " + description)

var tmpMsgArr = description.components(separatedBy: ".---")
let testcaseID = tmpMsgArr[0]

print("------" + testcaseID)

yourAwesomeMethodThatWillbeCalledWhenTCFails() // implement this method that you want to execute

failedTC += 1
}

}

Step 2: Add this observer to your test. For adding observer code is here.

  let observer = Observer() // Your created observer class
let observationCenter = XCTestObservationCenter.shared()
observationCenter.addTestObserver(observer)

Note: Remove the observer when you break down your tests or you will get multiple invocations and multiple calls on test events if you add an observer on every setUp.

In your case rewrite your set up method like below:

 override func setUp() {

super.setUp()
continueAfterFailure = false

let observer = Observer() // your created observer class
let observationCenter = XCTestObservationCenter.shared()
observationCenter.addTestObserver(observer)

self.launchWithUserLoggedIn()
}

Hope this complete example will help you.



Related Topics



Leave a reply



Submit