Monday, October 24, 2016

Playground and Testing Subquery by NSExpression (1)

I am not sure when Playground was introduced to Xcode, may version 2? I started to use Playground when I moved to Xcode 3. As I mentioned in my previous blog, I would like to find out a better way to test Subquery.

It is very easy to add a Playground in iOS project in Xcode. However, it is hard to test CoreData in Playground. First, CoreData model is not easy to add to Playground, even though I eventually found a way to add CoreData context to Playground. Next difficulty is that I still cannot fig out how to add CoreData managed data classes to Playground. It seems that namespace between my project and Playground are different. I could change all my managed classes to public, but they are not visible nor accessible through CoreData fetch mechanism.

My purpose is to test Subquery. When I found out that Subquery can be built in NSExpression, I immediately think about Playground as a place to do my test.

Playground in Xcode

To add a Playground to Xcode project is very simple: just add a new file to Xcode project and then select Playground. By default, it is called as MyPlayground. There are two folders within the Playground: Sources for all swift source codes, and Resources for resource files.

The following is MyPlayground in Xcode left panel Project navigator view.

On the right panel, the left area on the top is an editable area for swift codes. The right side on the top is an area for immediate result view and the bottom is debug result view.

It is better to organize all the classes, extensions, enumerations and types in swift files under Sources folder. In order to access them from Playground, all the accessible elements have to be public.

During your start test stage, you may need to place all your classes and variables, types, and functions in Playground. In this way, any run-time errors would be highlighted during your test. When all those elements are OK, you can move them to a swift file under Sources folder.

Test Class

As I mentioned in my previous blog, the first parameter in Subquery is a collection of objects, defined by a class. What I found that there are some requirement for the class. First, the class has to inherit from NSObject which has hash code for equality. Secondly, if you need to use operator ALL, ANY, or NONE on collection properties defined in the class, the collection has to be NSSet type.

For my test, I defined the following Person class.

  1. public class Person : NSObject {
  2.    public var name: String
  3.    public let birthYear: Int
  4.    public var friends: NSSet?
  5.    init(name pName: String, birthYear year : Int) {
  6.        name = pName
  7.        birthYear = year
  8.        super.init()
  9.    }
  10. }

I created a swift file TestSubquery.swift in Sources folder. The Person class is defined there. This class is perfect data for Subquery's first parameter: collection. The class has two simple properties name and birthYear, and one collection property friends. In my test, only one person object is created and placed in an array as collection, which is used as the first parameter in Subquery.

friends property is optional NSSet type collection, where actually Person objects are hold or null if no objects. Each person in friends could have friends. In this way, nested friends could be used as many levels of one-to-many relationship for testing. The following is my help function(in TestSubquery.swift file as a module function) to create base data for my tests. The function takes one parameter to control levels of nested friends. I'll explain how to use this parameter to do some interesting tests later.

  1. func getPerson(friendsLevel: Int) -> [Person] {
  2.    print("Creating Person object and friends...")
  3.    let p = Person(name: "Bob", birthYear: 1997)
  4.    if friendsLevel > 0 {
  5.        let p1 = Person(name: "Tony", birthYear: 2010)
  6.        if friendsLevel > 1 {
  7.            let p1_1 = Person(name: "Tony1", birthYear: 2012)
  8.            if friendsLevel > 2 {
  9.                 let p1_1_1 = Person(name: "Tony1_1", birthYear: 2011)
  10.                 let p1_1_2 = Person(name: "Tony1_2", birthYear: 2012)
  11.                 p1_1.friends = NSSet(objects: p1_1_1, p1_1_2)
  12.            }
  13.            let p1_2 = Person(name: "Lisa1", birthYear: 2011)
  14.            p1.friends = NSSet(objects: p1_1, p1_2)
  15.        }
  16.        let p2 = Person(name: "Lisa", birthYear: 2012)
  17.        p.friends = NSSet(objects: p1, p2)
  18.    }
  19.    printPerson(personCollection: [p], printFriends: true)
  20.    return [p]
  21. }

Here is the complete debug information of person data structure (up to 2 levels of nested friends):

  1. Creating Person object and friends...
  2. Person - name: Bob, birthYear: 1997
  3.    Friend - name: Tony, birthYear: 2010
  4.      Friend - name: Tony1, birthYear: 2012
  5.        Friend - name: Tony1_2, birthYear: 2012, friends: (none)
  6.        Friend - name: Tony1_1, birthYear: 2011, friends: (none)
  7.      Friend - name: Lisa1, birthYear: 2011, friends: (none)
  8.    Friend - name: Lisa, birthYear: 2012, friends: (none)

In addition to that, I have added some other types and help functions, as well as a test class TestSubquery. Here I don't need to list all the codes for them. Only test codes will be provided and explained in the following sections.

The complete source code can be downloaded from a link in Resources section.

Test Structure

Subquery NSExpression will be used as an expression object for my tests. To build a Subquery expression, two things have to be prepared: a collection data or expression and a predicate. Therefore, all my tests are defined in the following structure or steps:

  1. get an expression of Person collection,
  2. create a predicate, and
  3. build a Subquery expression, finally evaluate the Subquery expression to get result.

Step 1 is very simple. The collection of Person is obtained from the function getPerson(...) as explained above. In step 2, a predicate is created by one of a list of help functions, which define interested fetch query conditions against to the collection of Person. Finally, it is time ready to build a Subquery expression and then to evaluate its result in step 3.

Let's look at a simple test func defined in TestSubquery class.

  1. public class TestSubquery {
  2. ...
  3.  public static func TestSubquery(nameContains: String, birthYearLowLimit: Int, friendsLevel: Int = 2) {
  4.    let e = getColletionExpression(friendsLevel: friendsLevel)
  5.    let predicate = getPredicate(nameContains: nameContains, birthYearLowLimit: birthYearLowLimit)
  6.    let expValue = getSubqueryExpressionResult(collection: e, predicate: predicate)
  7.    printSubqueryExpressionWith(result: expValue)
  8.  }
  9. ...
  10. }

At line 4, only one person with a name, birth year and a collection of friends (or null if no friends) is created as collection expression by the help func of getCollectionExpression. At line 5, a predicate with CONTAINS(string) function for name and low limit for birth year is obtained from the help func of getPredicate. With the data collection and predicate ready, at line 6, the help func of getSubqueryExpressionResult builds a Subquery expression and evaluates the result.

Test and Result

To better understand my test, extensive information are printed out in debug window. Here is one example of using above test func in Playground:

TestSubquery.TestSubquery(nameContains: "L", birthYearLowLimit: 2010, friendsLevel: 0)

and the debug information is as follows:

  1. Creating Person object and friends...
  2. Person - name: Bob, birthYear: 1997
  3. Collection expression: {<MyPlayground_Sources.Person: 0x60800007e940>}
  4. Collection expression result value: (
  5.    "<MyPlayground_Sources.Person: 0x60800007e940>"
  6. )
  7. Person - name: Bob, birthYear: 1997
  8. Predicate: $ CONTAINS "L" AND $x.birthYear > 2010
  9. Subquery Expression: SUBQUERY({<MyPlayground_Sources.Person: 0x60800007e940>}, $x, $ CONTAINS "L" AND $x.birthYear > 2010)
  10.  collection: {<MyPlayground_Sources.Person: 0x60800007e940>}
  11.  variable: x
  12.  predicate: $ CONTAINS "L" AND $x.birthYear > 2010
  13. Subquery expression result value: Optional(<__NSArrayM 0x610000046030>(
  14. )
  15. )
  16. Subquery expression value as [Person]: [] as follows

In this test result, line 1-7 explain that only one person "Bob" who is born in 1997 created as in a collection expression. The debug information at line 8 is the print-out during a predicate was obtained. From line 9-12 are information about building Subquery expression, and its three parameters: collection, variable and predicate. The information at remaining lines shows the Subquery expression evaluation result, and detail information as an array of Person or empty if no result.

If test is changed to name containing "B" and birth year greater than 1990, the person Bob will be in the result array:

  1. Creating Person object and friends...
  2. Person - name: Bob, birthYear: 1997
  3. Collection expression: {<MyPlayground_Sources.Person: 0x608000077fc0>}
  4. Collection expression result value: (
  5.    "<MyPlayground_Sources.Person: 0x608000077fc0>"
  6. )
  7. Person - name: Bob, birthYear: 1997
  8. Predicate: $ CONTAINS "B" AND $x.birthYear > 1990
  9. Subquery Expression: SUBQUERY({<MyPlayground_Sources.Person: 0x608000077fc0>}, $x, $ CONTAINS "B" AND $x.birthYear > 1990)
  10.  collection: {<MyPlayground_Sources.Person: 0x608000077fc0>}
  11.  variable: x
  12.  predicate: $ CONTAINS "B" AND $x.birthYear > 1990
  13. Subquery expression result value: Optional(<__NSArrayM 0x60800005cc50>(
  14. <MyPlayground_Sources.Person: 0x608000077fc0>
  15. )
  16. )
  17. Subquery expression value as [Person]: [<MyPlayground_Sources.Person: 0x608000077fc0>] as follows
  18. Person - name: Bob, birthYear: 1997

It is really fun to test Subquery expression in Playground.

Stay tuned for the power of Subquery expression and more interesting findings in my other tests.