-
Notifications
You must be signed in to change notification settings - Fork 263
[SR-460] Provide XCTestCase a class to allow overriding setUp() and tearDown() #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SR-460] Provide XCTestCase a class to allow overriding setUp() and tearDown() #40
Conversation
Oh man, thank you for this proposal. The other day I was trying to implement A few questions:
I'm also very interested in what the core team thinks here. |
I have been intending to make a similar change as well. If we have XCTestCase as a base class, I am wondering what advantage we really get having the XCTestCase protocol (XCTestCaseType in your proposal), though, and even what benefit we’re really getting from XCTestCaseProvider. I had more or less planned to remove both the protocols in favor of implementing an XCTestCase base class. For me, these changes were motivated by wanting to implement XCTestObservation (as Brian G. talks about below.)
For XCTestObservation to be useful, we will need to do this. This will also most likely require that XCTMain change to take XCTestCase classes instead of instances. (And that will require some coordination of changes in the two main projects that are clients of XCTest… corelibs Foundation and the package manager).
|
760b98b
to
468177c
Compare
Cool. I had been trying to preserve the protocol to avoid locking things down too much, but it sounds like things are headed towards a place that requires a tighter interface. I've now pushed some modified commits which do away with the protocols as @mike-ferris-apple suggested. The original branch with the I have started prototyping some changes that would allow a new XCTestCase instance to be used for each test, but none of that is included in this PR. |
When we take this pull request, we're going to need existing clients to update code, right? (At least they'll be adding the "override" on their allTests, and on setUp() and tearDown() if they have them.) We're going to have to coordinate with the foundation and package manager projects to get patches for their tests to go in at the same time these changes go. When we change the execution semantics to an instance per test, we will also need to make another source breaking change. I think it might be good to do that at the same time so we will hopefully not have to do another coordinated patch of foundation and package manager at that time. I would recommend we change the signature of XCTMain() to be:
In addition, I found it useful to change allTests into a dictionary when I was prototyping the change to the execution model as well:
How do you feel about making these changes preemptively as well to minimize the number of changes we're having to coordinate with the two major clients? |
One additional detail about allTests as a dictionary... this removes ordering from allTests. To replace that, I was sorting the tests by name. This makes it less practical to order tests specifically, but actually that's arguably a bad thing to do, especially if/when a test harness starts allowing executing subsets of test cases. Allowing ordering dependencies between test cases work against having suites that can be decomposed cleanly. |
This forces all tests to have unique names. An interesting aspect of the current tuple-based API is that tests need not be uniquely named within a test case. I actually consider this to be a feature; XCTest/Xcode integration implicitly requires test selectors to be unique, which causes subtle problems when dynamically generating test methods. See: specta/specta#83 (comment) I'm not against requiring each test method name to be unique within a test case, provided there's a benefit to doing so. What's the upside here? |
The one problem with this is that the tests won't be portable to Darwin. Because there will be an override that isn't overriding anything. I find it quite nice that I can copy/paste most tests across and run them against the Foundation that ships with Mac OS X. |
Arg! Good point. Note that part of the motivation here was to solve the opposite problem with setUp() and tearDown(). Not sure what to do here. If we keep that in the TestProvider protocol then all tests need to do the double inheritance (and actually that probably creates its own problems with the portability of tests). One possibility would be to add allTests to XCTestCase in the overlay so it’s an override in Xcode as well. I am not thrilled with that and would want to talk it through more here and with the rest of the Xcode XCTest folks as well… Any other options? Mike
|
Can setUp and tearDown come from inheritance and allTests come from protocol conformance? |
We would probably then require an empty protocol of the same name on Darwin to avoid having clients |
Perhaps there’s a better way to do this, but in order to prototype the execution model I wound up needing to instantiate an instance just to get the test list. Then as the tests are being run an instance has to be created to actually run each test case. The problem was that you need to use the closure from allTests for the instance that is going to run each test, so the initial instance gathered up the names and then passed each one into the instance that was going to run an individual test and it would look the closure up in allTests for that instance to get a block bound to the right self. Having the dictionary allowed that lookup not to be linear. It may be that there’s a cleverer way than I could think of to do this better. It sucks to have to create N+1 instances as well… If we can’t come up with a different approach, we could keep allTests as an array if we were willing to search it linearly, but that winds up being n-squared on the number of tests since we have to search once for each test. Mike
|
Yeah, if we’re doing that, I’d rather add allTests to XCTestCase in Xcode… At least then people wouldn’t need to have two things in their inheritance list every time. Mike
|
I believe this is why, in Apple XCTest,
In my experience, the most time-expensive aspect of Apple XCTest was XPC communication between the XCTest driver, Xcode, and the XCTest host on the iOS/tvOS/watchOS device/simulator. My gut is to not worry too much about linear lookups until they demonstrate themselves to be a performance bottleneck--but that's just my two cents! 💸
Uh-oh, somebody better tell Joar. 😉 I'm hesitant to advocate adding this API to Apple XCTest. Perhaps we could migrate away from |
Someone with more generic Swift-fu than I might be able to come up with a way around this…
Mike |
Thanks for the discussion around this guys. I completely agree that we should aim to consolidate the breaking changes required for these two features. When I was hacking on the instance-per-test concept before, I ran into precisely the same issues that @mike-ferris-apple describes. I still have a couple of other ideas to work around the generics-related issues when making |
ced7f2b
to
7891df4
Compare
Sorry for the slow turnaround on this. After much wrangling with the type system, I have ended up with two approaches, either of which I would be happy to see merged. I've pushed my preferred option to this PR's branch, and the alternative one to another branch. Both branches include additional test cases covering the newly-introduced behaviors. To recap, the requirements I was aiming to meet are:
One set of changes is shared by the two branches. The two source code compatibility requirements demand that
The two branches vary in their handling of the This current branch achieves the goal of turning static var allTests: [(String, XCTestCaseType throws -> Void)] {
return [
("test_passes", test(test_passes)),
]
} In my opinion the only drawback to this approach is that it requires the use of this new The other branch keeps I'm looking forward to getting feedback on these branches. Note that in either case, documentation updates (particularly in the |
I wanted to at least reply as it may be a few days before I can look at these in detail. Conceptually the notion of saying test(testMethod) in the allTests seems like a small price to pay. I’ll try to shop this idea around with some folks here who may not be following this discussion as closely including the foundation and package manager folks. Thanks Brian! Mike
|
Thanks for the note, Mike. When you do get a chance to take a closer look, please consider whether these changes are compatible with preparing for the XCTestObservation work you mentioned in your first reply. I wasn't explicitly targeting that because I didn't fully understand the requirements you had in mind. But, for example, might it be necessary to make XCTestCaseType a class protocol? |
@@ -55,7 +55,9 @@ class FailingTestCase: XCTestCase { | |||
} | |||
} | |||
|
|||
extension PassingTestCase: XCTestCaseType {} | |||
extension FailingTestCase: XCTestCaseType {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the grand scheme of things, I feel this is fairly painless for end-users. 👍
If they're already writing cross-platform tests (like the swift-corelibs-foundation and swift-package-manager projects are), then they have a conditionally-compiled file named that's used to call XCTMain()
. This pull request simply requires those projects to add extensions like these in that file, one for each of their test cases.
Once we can use runtime reflection to automatically discover tests, we can simplify this quite a bit--we won't need to jump through hoops to ensure we don't override
allTests
. In the meantime, this seems like the best way to proceed.
51793b2
to
c6ef244
Compare
@czechboy0 interesting idea; I hadn't thought of using SwiftPM to find the test cases instead of building it into metadata in the runtime. |
@mike-ferris-apple This seems reasonable to me. CC @phausler |
the idea seems pretty solid as an approach. What would we need to do for swift-corelibs-foundation? How can one disable tests with this approach? or exclude them from certain platforms? So this means we can get rid of the main.swift in our test project and the |
@mike-ferris-apple I just opened swiftlang/swift-package-manager#158 and swiftlang/swift-corelibs-foundation#270 with the required changes for those projects. This should allow us to move forward with merging these changes in even before @czechboy0's (exciting!) work is ready to go. |
@briancroom 👍 Only a small change will be required in the new test manifest generator once this gets merged. |
This matches Darwin XCTest behavior
* Remove XCTestCase and XCTestCaseProvider protocols * Add XCTestCase class so `setUp` and `tearDown` can be marked `override` * Change `invokeTest` to be static and create new instances for each test * Add a generic type-erasure function adapting a list of static method references on a type to the required signature.
* Add XCTestCaseType conformances and update XCTMain invocation * Use the static allTests property
c6ef244
to
179d765
Compare
@swift-ci please test |
I just rebased them all now, so hopefully they will still be good to go then. |
[SR-460] Provide XCTestCase a class to allow overriding setUp() and tearDown()
OK. In that case, let's go now. All three have been merged. |
Awesome! Thanks for doing the honors @mike-ferris-apple 🎉 I appreciate everyone that's been involved in the discussions around this over the past weeks and thanks for your patience through all the iterations 😄 |
What is it?
This PR renames the
XCTestCase
protocol toXCTestCaseType
and introduces a newXCTestCase
class in its place which individual test cases may derive from. A relevant test case is also included (see below).Motivation
Providing
XCTestCase
as just a protocol rather than a class is a great concept, however it presents an insurmountable barrier to achieving source-level compatibility with Darwin XCTest with respect to definingsetUp()
andtearDown()
methods in test cases, because theoverride
keyword is required when inheriting (Darwin XCTest) but is prohibited when just conforming to the protocol (Corelibs XCTest). With this patch, it becomes possible to write a test case utilizingsetUp()
andtearDown()
which builds with both frameworks. Here is the example from SR-460:Drawbacks
There is one significant caveat to this change: test cases deriving from the new
XCTestCase
class must still explicitly declare conformance to theXCTestCaseType
protocol before they can be passed toXCTMain
, which makes this a breaking change for current users of Corelibs XCTest. (TheXCTestCase
base class can't conform toXCTestCaseType
without losing compile-time checking that individual test cases provide anallTests
property for their own tests.) I see two reasonable approaches to this for cross-platform tests:XCTestCaseType
on Apple platforms, and conform to the protocol in-line for each test case. This is essentially what swiftpm is currently doing (see here and here).XCTestCaseType
conformance in a separate source file (perhaps themain.swift
entry point which contains the call toXCTMain
). I can easily imagine running a script as a part of the build process which dynamically generates a source file adding this conformance to each test case class. Such a script could even be made to generate theallTests
property implementation automatically as well.Rejected Alternatives
I considered a couple of variants before settling on the current contents of this patch. Note that neither of these include the
XCTestCaseType
protocol:XCTestCase
protocol into a class which conforms toXCTestCaseProvider
. In this version, the baseXCTestCase
class included an emptyallTests
definition. This was rejected both because it required individual test cases to mark theirallTests
definitions asoverride
, and because there would be no compile-time checking that an overriddenallTests
was present at all.XCTestCase
protocol into a class which does not conform toXCTestCaseProvider
. In this implementation,XCTMain
remained unchanged, and accepted an array ofXCTestCase
classes. This fared poorly because it required a runtime check that individual test cases conform toXCTestCaseProvider
, which felt like a missed opportunity to leverage the type system.I look forward to getting feedback and iterating on this!
@mike-ferris-apple @eschaton @modocache