Search code examples
iosswiftxcodeunit-testingxcode10

Legacy ObjC/Swift codebase crashes tests in `swift_checkMetadataState`: how to disentangle app/test targets?


I've got a mixed ObjC/Swift codebase with a very mixed-up structure, and the tests no longer run under Xcode 10.2 (details below). I'm trying to make the minimum structural changes to get tests back, so that I have a safety net for the refactoring that will come next. After taking what seems to me to be the obvious first step, the tests fail to build (details again below).

I'm interested in any of

  • solutions for the original problem (to get tests running again in the messy setup)
  • solutions for the problem after refactoring (to get refactored tests building in the somewhat tidier setup)
  • suggestions for build settings to verify or include in the description here, to get clearer on what's going wrong

I'm not interested in advice about how to structure a fresh project or create new targets with more sensible configurations: extracting code from these targets is also non-trivial, so I really need test coverage back before I start any refactoring.

Original situation (very messy)

  • project myCompany
  • app target app which builds module MyCompany
    • contains both Swift and ObjC code, with dependencies both ways
    • Defines Module = Yes, Product Module Name = MyCompany
  • test target myCompanyTests
    • Defines Module = No, Product Module Name = MyCompany
    • also contains both Swift and ObjC code
  • CocoaPods for external dependencies, also a bunch of internal Swift modules with dependencies managed by hand

Test files are included only in the myCompanyTests target, but many code files are included in both app and myCompanyTests targets. I think this and the two targets defining the same module name was probably in order to avoid having to import the app target into tests, because of problems with Swift/ObjC interop (see the next section). Building tests produces warnings about classes implemented in two places:

objc[9724]: Class _TtC12MyCompany12DiaryFetcher is implemented in both
/Users/tikitu/Library/Developer/CoreSimulator/Devices/556CAC28-B90B-4B6B-A87C-1A1450795051/data/Containers/Bundle/Application/495F33C2-F7FC-4AE6-B3FE-6908D6361B55/mycompany-develop.app/mycompany-develop (0x10d724060) 
and 
/Users/tikitu/Library/Developer/Xcode/DerivedData/mycompany-bifciplpbqaeqqdrmhivpjgnojri/Build/Products/Debug-iphonesimulator/mycompany-develop.app/PlugIns/myCompanyTests.xctest/myCompanyTests (0x13566dc38). 
One of the two will be used. Which one is undefined.

As of Xcode 10.2, myCompanyTests builds successfully but running the tests fails with an EXC_BAD_ACCESS in swift_checkMetadataState somewhere inside the UIApplicationMain call. My guess is that this is related to the module-name/files-in-both-targets shenanigans.

First attempt at "fix the obvious mistakes"

As a first attempt to tidy things up somewhat I've done the following:

  • Remove all non-test files from the myCompanyTests target
  • Rename myCompanyTests Product Module Name to MyCompanyTests
  • Add @testable import MyCompany in lots of swift tests

Then I start running into Swift/ObjC interop problems, because I need to call Swift code in the app target from ObjC code in the test target. Things I've tried:

  • #import "MyCompany-Swift.h" in an objc .m test file
    • 'MyCompany-Swift.h' file not found
  • #import <MyCompany-Swift.h> in an objc .m test file
    • 'MyCompany-Swift.h' file not found
  • #import <MyCompany/MyCompany-Swift.h> in an objc .m test file (yes you can see I don't actually understand this mechanism)
    • 'MyCompany/MyCompany-Swift.h' file not found
  • #import "MyCompanyTests-Swift.h" in all objc .m test files that need access to the app target
    • the generated file MyCompanyTests-Swift.h includes a line @import MyCompany; which looks very promising!
    • but that line fails with the error Module 'MyCompany' not found

Especially this last one looks suspicious to me, as I would expect the Xcode generated file should "just work". I guess I've overridden some user setting somewhere that's getting in the way: I'd be delighted with any suggestions for places to check.


Solution

  • I believe the issue to be that only a framework target auto-creates the swift-to-objc header file Module-Swift.h: neither the app target nor a test target appear to do so.

    I successfully resolved these issues by creating two new framework targets * MyCompanyStuffThatUsedToBeInApp (containing everything that used to be in the app target except main.h), which does bidirectional swift/objc interop and * MyCompanyTesting (a framework target imported by the tests, which likewise does bidirectional swift/objc interop for test-only code).

    There may still be simpler ways to tidy this up, but this one at least is proven to work.