swiftswift-testing

Deinitializer cannot be declared in struct that conforms to 'Copyable'


After implementing unit tests in a Swift Testing file, I wanted to check for possible memory leaks using the following strategy (credit to this YouTube video for teaching it)

  1. Instantiate a weak var pointing to your class (a ViewModel in my case) upon initializing your test suite struct.
  2. When de-initializing ("tearing-down," in XCTest terms), add a nil #expects() check (XCTAssertNil() in XCTest terms), to ensure the strong reference has been let go of in memory (thereby preventing a memory leak).
import Testing
@testable import Quote_Droplet

@Suite("Single Quote View Model Tests") struct SingleQuoteViewModel_Tests {

    // other instance variables
    let sut: SingleQuoteViewModel
    weak var weakSUT: SingleQuoteViewModel?

    @MainActor init () {
        // other instance variables
        self.sut = SingleQuoteViewModel(
            // parameters passed into the ViewModel from the other instance variables
        )
        self.weakSUT = self.sut
    }

    deinit {
        #expect(weakSUT == nil)
    }
    // test functions
}

The issue is, that I get the compilation error, Deinitializer cannot be declared in struct 'SingleQuoteViewModel_Tests' that conforms to 'Copyable'

The fix suggested by the compiler is to "Consider adding ~Copyable to struct SingleQuoteViewModel_Tests". However, I believe the answer I've put below, as sourced from Apple's documentation is more applicable to what I'm trying to accomplish.


Solution

  • TL;DR: change the test struct to a class. Then, if your tests are failing, mark the test suite as .serialized (see this timestamped section of a YouTube video for the reason why):

    Before:

    @Suite("Single Quote View Model Tests") struct SingleQuoteViewModel_Tests {

    After (Fixed):

    @Suite("Single Quote View Model Tests", .serialized) final class SingleQuoteViewModel_Tests {

    My source for switching struct -> final class is from the Apple documentation on Swift Testing, which makes that suggestion here: "If teardown is needed, declare your test suite as a class or as an actor rather than as a structure and implement deinit".

    However, there's a caveat to this that Apple doesn't mention explicitly in that section, and what the YouTube video linked above explains in more detail, regarding parallel unit test execution messing with expected behaviour.

    Tip: if you don't need inheritance, then adding final for compiler optimization is a good idea (see this Stackoverflow answer explaining why).