How to expose XCTestCases to external test bundles

2019-06-07 19:19发布

问题:

I have a framework Whiteboard that encapsulates my business logic. I'm trying to keep my dependencies inverted and decoupled, so since Whiteboard depends on a repository, it declares a protocol WhiteboardRepository, and it expects clients that link against Whiteboard to supply an implementation of WhiteboardRepository.

You can see this in the screen shot below. Note also the WhiteboardTests group, which includes a WhiteboardRepositoryTests class along with a WhiteboardRepositoryFake and its own test subclass.

To ensure that implementations of WhiteboardRepository behave as expected, the WhiteboardTests test bundle defines a WhiteboardRepositoryTests subclass of XCTestCase:

class WhiteboardRepositoryTests: XCTestCase {
  var repo: WhiteboardRepository?

  override func setUp() {
    if let repo = repo {
      // test setup here
    }
  }

  // test cases here

}

In order for a client of Whiteboard to test its implementation of WhiteboardRepository, the test class for the implementation subclasses the WhiteboardRepositoryTests and supplies an instance of the implementation to the test subclass, which then uses that instance when running the tests.

For example, here's what WhiteboardRepositoryFakeTests looks like:

class WhiteboardRepositoryFakeTests: WhiteboardRepositoryTests {
    override func setUp() {
        repo = WhiteboardRepositoryFake()
        super.setUp()
    }

    // the test classes run, using the instance of WhiteboardRepositoryFake()
}

This works fine, of course, since WhiteboardRepositoryFakeTests is in the WhiteboardTests bundle, so WhiteboardRepositoryTests is exposed to WhiteboardRepositoryFakeTests.

The problem is: apps that link against Whiteboard will need to create their own subclass of WhiteboardRepositoryTests to test their own implementation, but because they don't have access to the WhiteboardTests test bundle, they're not aware of the WhiteboardRepositoryTests class and so can't subclass it.

I've got multiple clients consuming Whiteboard, so I can't simply copy the WhiteboardRepositoryTests class into each client—and nor would I want to, since it's the responsibility of Whiteboard to define WhiteboardRepository's behavior, so WhiteboardRepositoryTests should live in Whiteboard's test bundle. In an ideal world, I'd be able to link or inject Whiteboard's test bundle into a client's test bundle so that WhiteboardRepositoryTests is exposed to the client's tests, but I don't see how I can do this.

Is there any way around this obstacle? How can I expose WhiteboardRepositoryTests to tests within a client test bundle so that the client can make sure its implementation of WhiteboardRepository behaves as expected?

回答1:

The main problem here is that the WhiteboardTests target is likely a test target and does not actually build a linkable framework. This prevents us from building another target (your client tests) that imports and subclass from it. For example, you'll notice that a test target lacks the normal {TARGET_NAME}.h header file that is used to export symbols from the framework. What we need is a target that builds a framework but links with XCTest. Client projects' test targets will link with this framework to import and subclass the XCTestCase classes it provides. So what to do:

  1. Create a framework building target in your Whiteboard project by clicking the project file then Editor > Add Target. Let's call this WhiteboardAbstractTests for now. This target will link with XCTest and build a framework to supply the superclasses that you want your client projects' tests to subclass.

  2. Now, to link with XCTest is a bit weird because they don't make it readily available in Xcode anymore. However, you can still find the XCTest framework at Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework and drag this into the WhiteboardAbstractTests target folder in Xcode.

  3. From here, you should be able to define XCTestCase subclasses in WhiteboardAbstractTests that will be built into the WhiteboardAbstractTests.framework.

  4. In your client project, I assume that the Whiteboard xcodeproj is nested in some way, either by Cocoapods, Carthage, or git submodule. Find the Whiteboard xcodeproj file and drag it into the client project test target in Xcode.

  5. Select the client project file in Xcode and go to Build Phases. Under Target dependencies, click the plus button and you should see various build targets from Whiteboard. Select WhiteboardAbstractTests. This will make sure Xcode builds the abstract tests before building your clients tests.

  6. Also in Build Phases, under Link Binary With Libraries, select WhiteboardAbstractTests to make sure that our test target links with and can import classes from the abstract tests provided by Whiteboard.

From here you should be able to run the tests in your client project which will build WhiteboardAbstractTests, link your client tests with it, and run your client tests which subclass the abstract tests. I've quickly tested it out but let me know if you have trouble.

P.S. I like your architectural style. Somewhere Martin Fowler sheds a tear of joy.