How to run one-time setup code before executing any XCTest
Asked Answered
W

4

58

I have the following problem. I want to execute a piece of code before all test classes are executed. For instance: I don't want my game to use the SoundEngine singleton during executing, but the SilentSoundEngine. I would like to activate the SilentSoundEngine one time not in all tests. All my tests look like this:

class TestBasketExcercise : XCTestCase {        
    override func setUp() {
        SilentSoundEngine.activate () // SoundEngine is a singleton
    }
    // The tests 
}

-Edit- Most of the answers are directed at providing custom superclass for the TestCase. I am looking for a more general and cleaner way to provide the environment that all tests need to execute. Isn't there a "main" function/ Appdelegate like feature somewhere for tests?

Wilks answered 23/4, 2015 at 11:44 Comment(1)
I'm a little late to the party, but how about putting a dispatch_once() in your setUp() function?Troublemaker
H
82

TL;DR:
As stated here, you should declare an NSPrincipalClass in your test-targets Info.plist. Execute all the one-time-setup code inside the init of this class, since "XCTest automatically creates a single instance of that class when the test bundle is loaded", thus all your one-time-setup code will be executed once when loading the test-bundle.


A bit more verbose:

To answer the idea in your edit first:

Afaik, there is no main() for the test bundle, since the tests are injected into your running main target, therefore you would have to add the one-time-setup code into the main() of your main target with a compile-time (or at least a runtime) check if the target is used to run tests. Without this check, you'd risk activating the SilentSoundEngine when running the target normally, which I guess is undesirable, since the class name implies that this sound-engine will produce no sound and honestly, who wants that? :)

There is however an AppDelegate-like feature, I will come to that at the end of my answer (if you're impatient, it's under the header "Another (more XCTest-specific) approach").


Now, let's divide this question into two core problems:

  1. How can you ensure that the code you want to execute exactly one time when running the tests is actually being executed exactly one time when running the tests
  2. Where should you execute that code, so it doesn't feel like an ugly hack and so it just works without you having to think of it and remember necessary steps each time you write a new test suite

Regarding point 1:

As @Martin R mentioned correctly in his comments to this answer to your question, overriding +load is not possible anymore as of Swift 1.2 (which is ancient history by now :D), and dispatch_once() isn't available anymore in Swift 3.


One approach

When you try to use dispatch_once anyway, Xcode (>=8) is as always very smart and suggests that you should use lazily initialized globals instead. Of course, the term global tends to have everyone indulge in fear and panic, but you can of course limit their scope by making them private/fileprivate (which does the same for file-level declarations), so you don't pollute your namespace.

Imho, they are actually a pretty nice pattern (still, the dose makes the poison...) that can look like this, for example:

private let _doSomethingOneTimeThatDoesNotReturnAResult: Void = {
    print("This will be done one time. It doesn't return a result.")
}()

private let _doSomethingOneTimeThatDoesReturnAResult: String = {
    print("This will be done one time. It returns a result.")
    return "result"
}()

for i in 0...5 {
    print(i)
    _doSomethingOneTimeThatDoesNotReturnAResult
    print(_doSomethingOneTimeThatDoesReturnAResult)
}

This prints:

This will be done one time. It doesn't return a result.
This will be done one time. It returns a result.
0
result
1
result
2
result
3
result
4
result
5
result

Side note: Interestingly enough, the private lets are evaluated before the loop even starts, which you can see because if it were not the case, the 0 would have been the very first print. When you comment the loop out, it will still print the first two lines (i.e. evaluate the lets).
However, I guess that this is playground specific behaviour because as stated here and here, globals are normally initialized the first time they are referenced somewhere, thus they shouldn't be evaluated when you comment out the loop.


Another (more XCTest-specific) approach

(This actually solves both point 1 and 2...)

As the company from Cupertino states here, there is a way to run one-time-pre-testing setup code.

To achieve this, you create a dummy setup-class (maybe call it TestSetup?) and put all the one time setup code into its init:

class TestSetup: NSObject {
    override init() {
        SilentSoundEngine.activate()
    }
}

Note that the class has to inherit from NSObject, since Xcode tries to instantiate the "single instance of that class" by using +new, so if the class is a pure Swift class, this will happen:

*** NSForwarding: warning: object 0x11c2d01e0 of class 'YourTestTargetsName.TestSetup' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector +[YourTestTargetsName.TestSetup new]

Then, you declare this class as the PrincipalClass in your test-bundles Info.plist file: Declaring the PrincipalClass in Info.plist

Note that you have to use the fully qualified class-name (i.e. YourTestTargetsName.TestSetup as compared to just TestSetup), so the class is found by Xcode (Thanks, zneak...).

As stated in the documentation of XCTestObservationCenter, "XCTest automatically creates a single instance of that class when the test bundle is loaded", so all your one-time-setup code will be executed in the init of TestSetup when loading the test-bundle.

Holmes answered 27/1, 2017 at 13:24 Comment(8)
Everyone drinks one for each time I wrote test in the answer XDHolmes
I don't understand the downvote. Did the answer not work for someone or is there something I should improve or correct? :/Holmes
Thank you for such a detailed response. FYI - I found that with Xcode 8.3.3, using the fully qualified name (eg. YourTestTargetsName.TestSetup) for the NSPricipalClass DID NOT work, but the simple name DOES work (eg. TestSetup).Chef
For a good introduction to setting up your own test observer, check out this WWDC video -- skip ahead to the 5 minute mark for specific info related to this topic: developer.apple.com/videos/play/wwdc2016/409 The API entry on XCTestObservation also has an overview of the events you can observe with your class: developer.apple.com/documentation/xctest/xctestobservationGabbey
The details are at around 7:30. An important caveat regarding the test target name: your test target name seems to have to be a valid identifier in order for the loader to find the principal class, even though Xcode in general doesn't require that -- e.g. not FooTests macOS but FooTestsMacOS. Otherwise you'll get Info.plist specified <name> for NSPrincipalClass, but no class matching that name was found. There may be some name-mangling trick to get around this, but I don't know what it is.Detradetract
With Xcode 9.4.1 the target name is still required. I couldn't get it to work by just specifying the class name.Elgar
How can this be done in a Swift Package Manager package? It didn't give me an info.plist by default like it does for normal apps so I'm not sure how to add oneSutherland
Good question, I haven't tried that yet as I'm currently not working on packages. Anyone else?Holmes
P
71

From Writing Test Classes and Methods:

You can optionally add customized methods for class setup (+ (void)setUp) and teardown (+ (void)tearDown) as well, which run before and after all of the test methods in the class.

In Swift that would be class methods:

override class func setUp() {
    super.setUp()
    // Called once before all tests are run
}

override class func tearDown() {
    // Called once after all tests are run
    super.tearDown()
}
Phenyl answered 23/4, 2015 at 12:3 Comment(14)
To clarify - setUp and tearDown are run once for each test, not once for the whole test class.Everyplace
@DavidLord: The instance methods setUp/tearDown are run once for each test. The class methods setUp/tearDown are run once before/after all tests. In other words, the class methods are run once, no matter how many tests there are.Phenyl
Oh! Having seen this elsewhere now - yes, you're right. Fancy editing the answer to include samples for override func and override class func?Everyplace
Stupid question: are those super calls required in the class methods? Should super only be called on instantiated objects, not class methods? I can certainly see the case for saying that even with class methods, the class is put into memory and since the test class inherits from XCTestCase the super calls need to be there, but to a recovering Java programmer, this looks weird.Aerobatics
I agree with Professor Tom, this looks weird. Shouldn't it be calling the class method on the base class instead of super? Something like XCTestCase.setUp()?Spahi
@WadeMueller. It makes not difference. In a class method foo, super.foo() calls the class method foo() of the super class.Phenyl
@MartinR Thanks for the clarification, didn't know that.Spahi
@ProfessorTom: XCTestCase has a class func setUp() which is called once before any test method. If we override that in our subclass then we should call the superclass method as well.Phenyl
What have I to do if I need to do an asynchronous request in the class setUp with an expectation?Borlase
@Zeb, that's a different question, I guess, maybe you should post it as one :)Holmes
Also, the comments in the setUp() and tearDown() methods in the code-block of this answer are a bit misleading, since the class func setUp() and tearDown() are executed once for each testSuite. If the OP only has one, they would achieve the desired behaviour but as soon as they add a second one, they would have to extract the setup into a superclass, to make sure it is called before any test is run. This would also lead to the setup being executed twice (once for each subclass-test-suite).Holmes
can we add XCTestExpectation as well, if we want to download some data?Cordless
Warning: If you have tests spread across multiple files, this answer doesn't work well. You can't put this in just one file since you can't easily control which runs first. You can't copy it into each files because then these methods are run multiple times. And if you try putting it in a shared base class they are still run multiple times (still once per file). The XCTestObservationCenter answer is much better if you have multiple test files.Bluish
This solution also works with SwiftPM test targets, unlike the accepted answer, which is nice.Dig
P
6

If you want to call the setUp method only once for all UI Tests in the class, in Xcode 11 you can call override class func setUp()

This approach is part of Apple documentation for UI testing: https://developer.apple.com/documentation/xctest/xctestcase/understanding_setup_and_teardown_for_test_methods

Pontoon answered 17/10, 2019 at 12:34 Comment(0)
G
3

If you build a superclass for your test case to be based on, then you can run a universal setup in the superclass and do whatever specific setup you might need to in the subclasses. I'm more familiar with Obj-C than Swift and haven't had a chance to test this yet, but this should be close.

// superclass
class SuperClass : XCTestCase {        
    override func setUp() {
        SilentSoundEngine.activate () // SoundEngine is a singleton
    }
}

// subclass
class Subclass : Superclass {
    override func setUp() {
        super.setup()
    }
}
Guinevere answered 23/4, 2015 at 12:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.