5

I have a singleton in a swift project (yes, I know people don't like those but I'd appreciate if you can look past that for now).

I'm writing some unit tests that aren't testing that singleton but the functions that they're testing depend on the state of that singleton. The singleton is declared with a static let, and the constructor is private anyway so resetting it like that isn't an option.

I can set it up right if I'm running one unit test by just setting a variable the singleton reads from in the setUp() method, but the moment I try to run the tests for the module as a whole, it gets set up with the first setUp() that calls it, and then it doesn't get reinstantiated after that. So basically it's stuck in a state for the entire module, which doesn't make sense to me - I would have expected everything to get reset between tests.

Is there a way to force XCTest to reset the testspace to make sure this singleton gets reset every time a new test file is run, rather than when it moves to a new module?

Tejas Sharma
  • 462
  • 1
  • 4
  • 17
  • I think you just ran, head first, into exactly why people don't recommend to use Singletons, at least directly. See https://stackoverflow.com/a/52879968/3141234 for my recommendation for how to clean this up. – Alexander Oct 23 '18 at 20:19
  • @Alexander-ReinstateMonica although I agree with some sentiment on why singletons should not be used, in this case I think the problem is deeper. If his singleton is "*it's stuck in a state*" for duration of testing, it will be stuck the same way while running for real. In other words, that singleton has irreversible state - that's a problematic design right there. – timbre timbre Jan 07 '20 at 15:45
  • Eh, I'm okay with that. Limiting the number of state transitions in a system simplifies it. One way flows simplify things, and that's nice. For example, a `Shipment` would always go from `Pending` to `Packing` to `Shipped` to `Delivered`. It'll never go from `Shipped` to `Pending` in a real use case. If that were the case, I would argue it's better to just generate a new shipment altogether, and keep things flowing in the "happy" direction, rather than trying to make all possible state transitions possible (and dealing with all the testing fallout that entails) – Alexander Jan 07 '20 at 15:52

4 Answers4

2

Singletons are a form of dependency injection. Not a great one, but DI nonetheless. To regain control without changing your implementation immediately, add a method to reset your singleton. It can either change its guts, or it can release the static instance.

Then call this method from tearDown().

(Then you can work on passing in the singleton as a protocol, instead of having the leaf modules reach out and grab it.)

Jon Reid
  • 20,545
  • 2
  • 64
  • 95
2

I'm not sure if this is a good or correct way, but I used it when I wanted to test my singleton class. in your unit test class, consider adding an extension for your singleton class and in this extension add a method named reset. implement reset logic for your singleton in this method and call this reset method of your singleton in tearDown function of your test class. example code:

import XCTest
@testable import MyModule

class MySingletonTests {
    override func setUp() {
        //
    } 

    override func tearDown() {
        MySingleton.shared.reset()
    }

    func testSomething() {
        //
    }
}

extension MySingleton {
    func reset() {
        // reset logic
    }
}
0

what you got there is a violation of the Dependency Inversion Principle.

Or in other words:

  • Create a protocol that the singleton is implementing (simply declare all methods and properties that you will use as a protocol and declare the singleton class to conform to it)

  • Every type that needs to use the singleton gets either a parameter typed as the protocol in its init's parameter list or has a property that is types as the protocol. In both cases save the singleton to that property.

  • when you create an object that needs the singleton, pass it.

  • you can use a default parameter, some very useful syntactical sugar. This will allow you to pass the singleton without needing to specify it over and over again.

  • in unit test you pass an object that implements the protocol but only mocks it functionality — a so-called mock.

vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
  • You can do DI without needing to explicitly inject each dependency explicitly. They is to just not access singletons directly, but through protocol-abstracted references, which allow for mocks to be substituted. See [my answer here for more details.](https://stackoverflow.com/a/52879968/3141234) – Alexander Oct 23 '18 at 20:19
0

Singleton problems aside, you should be able to get the Singleton re-instantiated at the start of each test by setting it to nil in tearDown() (providing that would bring its reference count to 0).

override func tearDown() {
    singleton = nil
    super.tearDown()
}

The reason you are only seeing this problem when running multiple tests is that you are not able to overwrite state leftover from the previous test since each test has its own instance of the test class.

For each class, testing starts by running the class setup method. For each test method, a new instance of the class is allocated and its instance setup method executed.

When you run one test by itself, one instance of your test class is created, and the singleton is initialized the same number of times as there are tests. However, when you run the whole test class, one instance of your test class is created for each test in the class (5 tests = 5 instances of your test class) but the singleton is only initialized once instead of 5 times. Each additional test class instance is created after the previous test finishes running, but persists for as long as it takes the test class to finish running. For non-singleton objects, this isn't a problem because a new, unrelated instance will be created for each test, but because you are using a Singleton, the same instance persists across tests and you do not get a chance to re-initialize it unless it gets deallocated. Only the first test is able to initialize the singleton into its desired state, so all the test of your tests fail.

See here for more information.

Oletha
  • 7,324
  • 1
  • 26
  • 46