Apple Concurrency framework is lacking support for async version of XCTAssertThrowsError. I have created a public function as a substitute for it.
func XCTAssertThrowsErrorAsync<T>(_ expression: #autoclosure () async throws -> T,
_ message: #autoclosure () -> String = "",
_ handler: (_ error: Error) -> Void = { _ in },
file: StaticString = #filePath,
line: UInt = #line) async {
do {
_ = try await expression() as T
XCTFail(
message(),
file: file,
line: line
)
} catch {
handler(error)
}
}
I would like to cover this function with unit tests.
What would be the best way to do it?
It depends.
If this is internal to your project: I extract custom test assertions from existing test cases. Then I manually break code to confirm that the assertion works the way I want it to. I especially want to see if I can make failure messages more informative.
I do this manually, without automation.
But if you intend to publish this to open source, then it makes sense to get test coverage. The goal would be to give developers who are not working with you immediate feedback on changes they make to your test framework.
I assume you know how to test the handler(error) call. (If you don't, let me know so I can add to this answer.) The problem is, how do we test the call to XCTFail? Here's how we did it to write ApprovalTests.Swift:
import XCTest
public protocol Failer {
func fail(_ message: String, file: StaticString, line: UInt) throws
}
public struct XCTFailer: Failer {
public func fail(_ message: String, file: StaticString, line: UInt) throws {
XCTFail(message, file: file, line: line)
}
}
Source: https://github.com/approvals/ApprovalTests.Swift/blob/master/ApprovalTests.Swift/Approvers/TestFailer.swift
Instead of calling XCTFail directly, we introduced a Failer protocol. By default, our code uses an XCTFailer() which calls the XCTFail for real. To test it, inject a Test Spy instead.
Related
I'm working in a fresh Kotlin Multiplatform mobile project, and I am having trouble implementing a Kotlin interface into a Swift class.
Here is my setup:
From kotlin common (shared) module:
interface LocalUserSource {
suspend fun saveUser(user: User): Boolean
suspend fun readUser(): User?
}
Implementing the protocol in Swift (I believe the protocol is generated by Kotlin/Native):
class DBUserSource : LocalUserSource {
func readUser(completionHandler: #escaping (common.User?, Error?) -> Void) {
// read user from core data
}
func saveUser(user: common.User, completionHandler: #escaping (KotlinBoolean?, Error?) -> Void) {
// save user with core data
}
}
The Xcode project is able to see the generated common framework, and I am able to jump to class / protocol definitions within the framework
But building the Xcode project continually results in this error:
Type 'DBUserSource' does not conform to protocol 'LocalUserSource'
When I use the "Fix" option in Xcode, it continually duplicates the method over and over and shows the same error. I've tried everything to clean both android studio (where I'm running the gradle build) and Xcode.
What's odd is, I've seen this work. I've saved and read users to core data, but today I cannot get the iOS side of things to work. Just wondering if anyone has experienced anything similar, and has any pointers.
Also here is the objective-c definition from the common framework:
__attribute__((swift_name("LocalUserSource")))
#protocol CommonLocalUserSource
#required
- (void)readUserWithCompletionHandler:(void (^)(CommonUser * _Nullable_result, NSError * _Nullable))completionHandler __attribute__((swift_name("readUser(completionHandler:)")));
- (void)saveUserUser:(CommonUser *)user completionHandler:(void (^)(CommonBoolean * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("saveUser(user:completionHandler:)")));
#end;
suspend fun readUser(): User? is a nullable in your Kotlin code, whereas you're using a non-nullable/non-optional type in the Swift equivalent function signature:
func readUser(completionHandler: #escaping (common.User, Error?) -> Void) {
// read user from core data
}
// The above should be
func readUser(completionHandler: #escaping (common.User?, Error?) -> Void) {
// read user from core data
}
So I finally figured it out. I had a generic Result class in my common module that looked like this:
sealed class Result<out T : Any>
class Success<out T : Any>(val data: T) : Result<T>()
class Error(private val exception: Throwable, val message: String? = exception.message) : Result<Nothing>()
inline fun <T : Any> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(data)
return this
}
inline fun <T : Any> Result<T>.onError(action: (Error) -> Unit): Result<T> {
if (this is Error) action(this)
return this
}
Once I removed this, I no longer saw the implementation error in the Swift code and the project ran. Honestly, no idea why. I assume something with generics and Kotlin/Native. But, if anyone has any idea, I'd love to know!
I have setup a Kotlin Multiplatform project and attached a SQLDelight database to it. Its all setup and running correctly as i have tested it on the android side using the following:
commonMain:
val backgroundColorFlow: Flow<Color> =
dbQuery.getColorWithId(BGColor.id)
.asFlow()
.mapToOneNotNull()
which triggers fine in the Android projects MainActivity.kt using:
database.backgroundColorFlow.onEach { setBackgroundColor(it.hex) }.launchIn(lifecycleScope)
but when trying to access the same call in the iOS projects app delegate i get the following options and im unsure how to use them or convert them into my BGColor object:
database.backgroundColorFlow.collect(collector: T##Kotlinx_coroutines_coreFlowCollector, completionHandler: (KotlinUnit?, Error?) -> Void)
can anyone help me with how to use this?
So this was resolved by creating a flow helper:
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun <T> Flow<T>.asCommonFlow(): CommonFlow<T> = CommonFlow(this)
class CommonFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
My backgroundColorFlow var is update as follows to utilise this helper:
val backgroundColorFlow: CommonFlow<BGColor> =
dbQuery.getColorWithId(BGColor.id)
.asFlow()
.mapToOneNotNull()
.map { BGColor(it.name) }
.asCommonFlow()
Then my swift works as follows:
database.backgroundColorFlow.watch { color in
guard let colorHex = color?.hex else {
return
}
self.colorBehaviourSubject.onNext(colorHex)
}
and android like so:
database.backgroundColorFlow.watch { setBackgroundColor(it.hex) }
Hope this helps anyone that comes across this. I would like to convert the CommonFlow class into an extension of Flow but don't have the know-how atm so if any could that IMHO would be a much nicer solution
You can do it in swift, with the mentioned collect method
FlowCollector is a protocol which can be implemented to collect the data of the Flow object.
Generic example implementation could look like:
class Collector<T>: FlowCollector {
let callback:(T) -> Void
init(callback: #escaping (T) -> Void) {
self.callback = callback
}
func emit(value: Any?, completionHandler: #escaping (KotlinUnit?, Error?) -> Void) {
// do whatever you what with the emitted value
callback(value as! T)
// after you finished your work you need to call completionHandler to
// tell that you consumed the value and the next value can be consumed,
// otherwise you will not receive the next value
//
// i think first parameter can be always nil or KotlinUnit()
// second parameter is for an error which occurred while consuming the value
// passing an error object will throw a NSGenericException in kotlin code, which can be handled or your app will crash
completionHandler(KotlinUnit(), nil)
}
}
The second part is calling the Flow.collect function
database.backgroundColorFlow.collect(collector: Collector<YourValueType> { yourValue in
// do what ever you want
}) { (unit, error) in
// code which is executed if the Flow object completed
}
probably you also like to write some extension function to increase readability
I'm writing a logging function that also calls a CocoaLumberjack function (yes, I know about custom loggers and chose not to use it). My function is using a forward, but I have a question about how the 'forwarding' works.
Here's my functions:
public func MyLogDebug(_ message: #autoclosure () -> String) {
// some code
MyLogMessage(message(), .debug)
}
public func MyLogMessage(_ message: #autoclosure () -> String, flag: DDLogFlag) {
// some code
if(myLogLevel.rawValue & flag.rawValue != 0) {
DDLogDebug(message())
}
}
My question is about the MyLogMessage(message(), .debug) function call. I know that avoiding string concatenation is really helpful for performance with logs, and I can see that inside MyLogMessage the string closure is only ran if the log level is passed. However, it look like in MyLogDebug the closure is also being evaluated.
I don't want to run the closure in MyLogDebug, so I tried to change it to MyLogMessage(message, .debug), but xcode gives me an error: Add () to forward #autoclosure parameter.
Will my code above evaluate the closure inside MyLogDebug? If so, is there another way to forward the closure parameter without evaluating it?
Will my code above evaluate the closure in MyLogDebug?
No. The call message() will be wrapped in a closure by the #autoclosure in MyLogMessage and only evaluated when message() is subsequently called in MyLogMessage.
Here is a little standalone example to play with:
func DDLogDebug(_ message: String) {
print("in DDLogDebug")
print(message)
print("leaving DDLogDebug")
}
public func MyLogDebug(_ message: #autoclosure () -> String) {
// some code
print("in MyLogDebug")
MyLogMessage(message(), flag: true)
print("leaving MyLogDebug")
}
public func MyLogMessage(_ message: #autoclosure () -> String, flag: Bool) {
// some code
print("in MyLogMessage")
if (flag) {
DDLogDebug(message())
}
print("leaving MyLogMessage")
}
MyLogDebug({ print("evaluated"); return "error message" }())
Output:
in MyLogDebug
in MyLogMessage
evaluated
in DDLogDebug
error message
leaving DDLogDebug
leaving MyLogMessage
leaving MyLogDebug
Note that the initial closure passed to MyLogDebug isn't evaluated until the DDLogDebug(message()) in MyLogMessage().
If you change the flag to false in the call to MyLogMessage(), the initial closure will never be evaluated.
I have a several protocols set up in my framework to deal with resources. In one of the protocols, I have set up an extension to provide a default implementation for a decode function. It's simpler to show the code and what happens (see calls to fatalError). There's a lot more code in the actual implementation, but this illustrates the issue:
This is the "base" protocol:
public protocol Resourceful {
associatedtype AssociatedResource
typealias ResourceCompletionHandler = (AssociatedResource?, Error?) -> Void
func fetch(_ completion: #escaping ResourceCompletionHandler)
}
This is a generic, concrete implementaion of Resourceful:
open class WebResourceApiCall<Resource>: Resourceful {
public typealias AssociatedResource = Resource
public typealias FetchedResponse = (data: Data?, urlResponse: URLResponse?)
public init() {
}
public func fetch(_ completion: #escaping ResourceCompletionHandler) {
try! decode(fetched: (data: nil, urlResponse: nil))
}
public func decode(fetched: FetchedResponse) throws -> Resource {
fatalError("It ends up here, but I don't want it to!")
}
}
extension WebResourceApiCall where Resource: Decodable {
public func decode(fetched: FetchedResponse) throws -> Resource {
fatalError("This is where I want it to go...")
}
}
This is how I'm attempting to use it:
public struct Something: Decodable { }
var apiCall = WebResourceApiCall<Something>()
apiCall.fetch { _, _ in } // Implictly calls decode... but not the decode I expected it to! See fatalError() calls...
Instead of calling decode on the extension, like I hoped it would, the "default" decode method with no constraints is always called.
Why doesn't this work the way I expect it to?
Thanks in advance!
Swift is a statically dispatched language, thus the address of the decode() function to be called is computed at compile time, and because the call happens inside the base definition of the class, the compiler picks the original implementation.
Now, if you call the method from a place where the compiler has enough information to pick the implementation you need, it will work:
var apiCall = WebResourceApiCall<Something>()
try apiCall.decode(fetched: (nil, nil))
The above code will call the method from the specialized extension, as at this point the compiler is a better position to know that it has a more specialized implementation to call.
It should be possible to achieve the behaviour you need if you move the decode() method in the dynamic dispatch world - i.e. at the protocol level.
I am currently writing a test for a bug I've encountered, where the order of calls in the production code is incorrect, leading to a potential race condition.
What is the cleanest way to check the order of calls from the test code, using XCTest?
In OCMock/Objective-C we had setExpectationOrderMatters, as per this question. However I am not aware of similar functionality available in XCTest/Swift due to dynamic/static language differences.
Let's say we want to mock this protocol:
protocol Thing {
func methodA()
func methodB()
}
Here's a mock that doesn't just record call counts of individual methods. It records invocation order:
class MockThing: Thing {
enum invocation {
case methodA
case methodB
}
private var invocations: [invocation] = []
func methodA() {
invocations.append(.methodA)
}
func methodB() {
invocations.append(.methodB)
}
func verify(expectedInvocations: [invocation], file: StaticString = #file, line: UInt = #line) {
if invocations != expectedInvocations {
XCTFail("Expected \(expectedInvocations) but got \(invocations)", file: file, line: line)
}
}
}
This supports test assertions like:
mock.verify(expectedInvocations: [.methodA, .methodB])