NSPredicate allows us to write predicates for validating a certain outcome. They’re often used combined with Core Data fetch requests and require a certain knowledge for writing custom formats. Besides my earlier shared Unit tests, best practices in Xcode and Swift predicates can be useful when writing unit tests.
We can benefit from using predicates when writing unit tests by converting them into an XCTestExpectation. An expectation when writing unit tests defines a certainly expected fulfillment of a certain event. In this case, we ‘expect’ the predicate to be ‘true’. By using closure-based predicates, we are no longer required to know more about writing custom predicate formats.
What is an NSPredicate?
NSPredicate is described by Apple as follows:
A definition of logical conditions used to constrain a search either for a fetch or for in-memory filtering.
Conditions are defined which are validated against a certain object. There are different kinds of predicates, like NSCompoundPredicate, and NSComparisonPredicate which we’re ignoring for today. They are worth looking into when writing more advanced predicates, for which the Predicate Programming Guide is a great start.
Writing an NSPredicate using a custom format
Predicates can be written using a defined format. In the following example, we’re writing a unit test to wait for an article to be published after a network call. Our predicate format writes a boolean check to see whether isPublished
is set to true:
final class Article: NSObject {
@objc var isPublished: Bool
init(isPublished: Bool) {
self.isPublished = isPublished
}
}
struct ArticlePublisher {
func publish(_ article: inout Article) {
article.isPublished = true
}
}
class NSPredicateTests: XCTestCase {
/// It should mark an article as published.
func testArticlePublished() throws {
let publisher = ArticlePublisher()
var article = Article(isPublished: false)
let predicate = NSPredicate(format: "%K == YES", #keyPath(Article.isPublished))
let publishExpectation = XCTNSPredicateExpectation(predicate: predicate, object: article)
publisher.publish(&article)
wait(for: [publishExpectation], timeout: 10.0)
}
}
Note that for the sake of this example, we’re mimicking the article publisher. In a real application, this would likely be an asynchronous network call.
As you can see, we’ve created an NSPredicate using a format %K == YES
. The %K
refers to the input key path isPublished
. The predicate can be passed into the XCTNSPredicateExpectation
type which transforms it into an XCTestExpectation
which we can use in our unit test.
Although this works and looks great, it comes with a few downsides:
#keyPath
only works with@objc
marked properties- Key path based predicates only work with
NSObject
types - The format is not type checked which makes it easy to write mistakes
Without thinking much further into formats, I’d like to dive into closure-based predicates. Formerly known as NSBlockPredicate, they allow us to use any type, checks and benefit from compile type validation.
Using closure based predicates
NSPredicate comes with an initialiser which takes a closure as input. The closure takes two arguments:
- evaluatedObject: The object to be evaluated
- bindings: The substitution variables dictionary containing key-value pairs for all variables in the receiver
In our case, we can ignore both parameters and directly reference the object we’re validating. The above test example would be written as follows:
/// It should mark an article as published.
func testArticlePublished() throws {
let publisher = ArticlePublisher()
var article = Article(isPublished: false)
let predicate = NSPredicate(block: { _, _ -> Bool in
return article.isPublished == true
})
let publishExpectation = XCTNSPredicateExpectation(predicate: predicate, object: article)
publisher.publish(&article)
wait(for: [publishExpectation], timeout: 10.0)
}
The best thing of all: we can rewrite our Article
to a struct and it all just works! There’s no longer a requirement of referencing an @objc
parameter or an NSObject deferred type. In other words, it should be usable in most of the cases you want to wait for a certain condition to be true.
So how does this work?
The NSPredicate is revisited every second to check whether the given block returns true for as long as the wait timeout has passed by. The expectation is fulfilled as soon as the closure returns true.
Creating a convenience method for writing predicate based expectations
By creating a convenience initialiser on XCTestCase
we allow ourselves to more easily create expectations for certain conditions. The implementation makes use of the XCTNSPredicateExpectation
in combination with the earlier written predicate.
extension XCTestCase {
/// Creates an expectation for monitoring the given condition.
/// - Parameters:
/// - condition: The condition to evaluate to be `true`.
/// - description: A string to display in the test log for this expectation, to help diagnose failures.
/// - Returns: The expectation for matching the condition.
func expectation(for condition: @autoclosure @escaping () -> Bool, description: String = "") -> XCTestExpectation {
let predicate = NSPredicate { _, _ in
return condition()
}
return XCTNSPredicateExpectation(predicate: predicate, object: nil)
}
}
The extension method takes an escaping @autoclosure as an argument. If you’re new to this technique, I encourage you to check out my article How to use @autoclosure in Swift to improve performance.
Rewriting our earlier example shows that our code is more readable while taking less code:
/// It should mark an article as published.
func testArticlePublished() throws {
let publisher = ArticlePublisher()
var article = Article(isPublished: false)
let publishExpectation = expectation(for: article.isPublished)
publisher.publish(&article)
wait(for: [publishExpectation], timeout: 10.0)
}
Waiting for asynchronous state changes becomes much more simple and becomes easier to write.
Conclusion
Predicate-based testing allows validating certain conditions to be true after an asynchronous event occurred. By writing a custom extension on XCTestCase
we allow ourselves to simplify NSPredicate based expectation creation. Validating conditions become both more readable and easier to write.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!