La fabrique mobile Viseo Toulouse

Playground Driven Development (Part 2)

See how Xcode Playgrounds can be used to do TDD (Test Driven Development).

This article is the second part of our "Playground Driven Development" serie. The code for the following article can be found in this repository (continuing from first part):

https://github.com/giln/Playgrounds-Driven-Development-Part2

Now we are going to write the code to download and parse the json from Apple feed.

To do this we are going to use a new Playground (Right click on Playgrounds Directory in project structure and click "Add File" then browse to Blank Playground). You can name this new Playground AppStoreRessourceTest for example.

We are going to do TDD with Xcode Playgrounds:

import UIKit
import XCTest

We want to write a simple AppStoreRessource class with one function to download json from Apple Feed and return a list of Applications with a completion handler.

We start with a test case for our class:

class AppStoreRessourceTest: XCTestCase {
    override func setUp() {
        super.setUp()
    }
    override func tearDown() {
        super.tearDown()
    }

    func testAppStoreRessource() {
        XCTAssertNotNil(AppStoreRessource())
    }
}

To remove the compiling error we have to write our AppStoreRessource class:

public class AppStoreRessource {

}

To run the tests from Xcode Playgrounds just add the line:

AppStoreRessourceTest.defaultTestSuite.run()

and execute using Cmd-Shift-Enter:

TestSkeleton.jpg

In the debug Area of our Playgrounds we see the results of our tests:

Test Suite 'AppStoreRessourceTest' started at 2018-11-26 17:08:59.236
Test Case '-[__lldb_expr_1.AppStoreRessourceTest testAppStoreRessource]' started.
Test Case '-[__lldb_expr_1.AppStoreRessourceTest testAppStoreRessource]' passed (0.001 seconds).
Test Suite 'AppStoreRessourceTest' passed at 2018-11-26 17:08:59.239.
	 Executed 1 test, with 0 failures (0 unexpected) in 0.001 (0.003) seconds

-> Green So we can now start TDD with our AppStoreRessource class by testing our getTopApps function:

func testGetTopAppsCompletes() {
        let getExpectation = self.expectation(description: "getTopApps Completed")

        AppStoreRessource().getTopApps { (_, _) in
            getExpectation.fulfill()
        }

        self.wait(for: [getExpectation], timeout: 1.0)
    }

Since the function does not exist yet, we have a compilation error:

CompilationError.jpg

Which can be fixed by adding the function to our AppStoreRessource class:

public func getTopApps(completion: @escaping ([App], Error?) -> Void) {
    }

We still have a compilation error because now, the "App" struct does not exist. So we add it along with it's testcase:

public struct App {}

class AppTest: XCTestCase {
    func testApp(){
        XCTAssertNotNil(App())
    }
}

So now the code compiles but if we run it, the tests fail with 1 error:

Test Case '-[__lldb_expr_17.AppStoreRessourceTest testGetTopAppsCompletes]' failed (1.014 seconds).

We complete our function with the minimum requirement to make the test green:

public func getTopApps(completion: @escaping ([App], Error?) -> Void) {
        completion([],nil)
    }

Rerun and our tests are now green!

How is our getTopApps going to get it's data?

We can use NSURLSession. But we want to stub our network data for our tests. How can we do this?

We start by writing a protocol that will allow us to mock the calls to the network:

public protocol DataFetching {
    func fetchData(url: URL, completion: @escaping (Data?, Error?) -> Void)
}

Then we inject a Datafetching object as a dependency to our AppStoreRessource class:

public class AppStoreRessource {
    // MARK: - Private Variables

    private let datafetcher: DataFetching

    init(datafetcher: DataFetching) {
        self.datafetcher = datafetcher
    }

    public func getTopApps(completion: @escaping ([App], Error?) -> Void) {
        completion([], nil)
    }
}

For real networking we can use NetworkFetcher class:

class NetworkFetcher: DataFetching {
    func fetchData(url: URL, completion: @escaping (Data?, Error?) -> Void) {
        let session = URLSession.shared

        session.dataTask(with: url) { (data, response, error) in
            DispatchQueue.main.async {
                completion(data, error)
            }
            }.resume()
    }
}

Which can be replaced for our tests with a MockFetcher class:

class MockFetcher: DataFetching {
    public var mockData: Data?
    public var mockError: Error?

    func fetchData(url: URL, completion: @escaping (Data?, Error?) -> Void) {
        completion(mockData, mockError)
    }
}

To compile our Playgrounds we need to modify and refactor our testcase:

class AppStoreRessourceTest: XCTestCase {
    private var appStoreRessource: AppStoreRessource!

    override func setUp() {
        super.setUp()

        let mockFetcher = MockFetcher()
        appStoreRessource = AppStoreRessource(datafetcher: mockFetcher)
    }

    override func tearDown() {
        super.tearDown()
    }

    func testAppStoreRessource() {
        XCTAssertNotNil(appStoreRessource)
    }

    func testGetTopAppsCompletes() {
        let getExpectation = self.expectation(description: "getTopApps did not complete")

        appStoreRessource.getTopApps { _, _ in
            getExpectation.fulfill()
        }

        self.wait(for: [getExpectation], timeout: 1.0)
    }
}

Run and green!

Let's now begin to do our Json parsing. The start of the json received from Apple looks like this:

{
	"feed": {
		"author": {
			"name": {
				"label": "iTunes Store"
			},
			"uri": {
				"label": "http://www.apple.com/itunes/"
			}
		},
		"entry": [{
				"im:name": {
					"label": "Toca Hair Salon 3"
				},
				"im:image": [{
						"label": "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/53x53bb-85.png",
						"attributes": {
							"height": "53"
						}
					},
					{
						"label": "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/75x75bb-85.png",
						"attributes": {
							"height": "75"
						}
					},
					{
						"label": "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/100x100bb-85.png",
						"attributes": {
							"height": "100"
						}
					}
				],
				"summary": {
					"label": "Welcome to Toca Hair Salon 3! Our most popular app series returns, better than ever! What do you feel like creating today — something quirky, pretty or edgy? Choose your character from dozens of looks that you can customize — characters have randomized styles and personalities, which creates endless play opportunities! Grab your tools and get styling!\n\nAMAZINGLY REALISTIC HAIR\nThe hair in Toca Hair Salon 3 looks and moves like real hair! You can style silky straight hair, bouncy waves, crinkly curls, and for the first time in the Toca Hair Salon series: kinky hair! This super-curly hair type means you can create fluffy 'fros and other natural hairstyles.\n\nAWESOME STYLING TOOLS\nNo salon would be complete without the right tools!\n\nBasics: Of course you'll find everything for a good wash, including shampoo and a blow dryer. You'll also find scissors, clippers, a razor, a brush and a multipurpose combing tool to use on your client's hair. In case you cut off a little too much, we've brought back our amazing grow tonic to regrow hair — definitely customer pleaser!\n\nHair types: The electric styling tools let you move between the different hair types to get just the right look.\n\nBraids: Use the brand-new braiding tools for more styling options! You can make thick braids or thin braids!\n\nBeards: Visit the beard station for the best in beard-grooming (or growing!). Lather up with shaving cream, then use scissors, clippers or a razor to get just the right length. Every character can have a beard!\n\nColor: Ready for some color? Toca Hair Salon 3 introduces a more advanced hair color tool with two different cans for different sized sprays. And the new rainbow spray lets you create the most colorful styles you can imagine!\n\nCLOTHES AND ACCESSORIES\nNow characters can change into different clothes! Try on new looks to suit that new hair style! Accessorize the look with glasses, caps, headbands and silly stuff. Choose a photo booth background and take a picture to save to your device and share!\n\nFEATURES\n- All-new characters with dozens of starter styles\n- Straight, wavy, curly and now kinky hair!\n- All the tools you need to create the style you want!\n- New braiding tool!\n- Every character can grow a beard to style\n- More advanced hair color tool: Dip-dye and fade hair in any colors you want!\n- Change clothes on characters!\n- Dozens of new accessories: hats, glasses, jewelry and more\n- Change backdrops in the photo booth!\n- Snap a picture in the app and share with your friends!\n- Gender-neutral aesthetic: Unlike most hairstyling apps, all kids are welcome to play!\n- No time limit or high scores — play for as long as you like!\n- No third-party advertising\n\nABOUT TOCA BOCA\nToca Boca is an award winning game studio that makes digital toys for kids. We think playing and having fun is the best way to learn about the world. Therefore we make digital toys and games that help stimulate the imagination, and that you can play together with your kids. Best of all - we do it in a safe way without third-party advertising or in-app purchases.\n\nPRIVACY POLICY\nPrivacy is an issue that we take very seriously. To learn more about how we work with these matters, please read our privacy policy: https://tocaboca.com/privacy"
				},
...

I see that the data i'm interested (applications) is wrapped into an "entry" array inside a "feed" dictionary.

I will use Swift 4 Decodable protocol to decode my Json in my model Object.

I will first test i get a "networkError" if no data is returned from my fetchData.

func testGetTopAppsCompletesWithNetworkError() {
    let getExpectation = self.expectation(description: "getTopApps did not complete with network error")

    // We don't pass any data to the mock on purpose
    appStoreRessource.getTopApps { _, error in
        if case .some(.networkError) = error {
                getExpectation.fulfill()
            }
    }

    self.wait(for: [getExpectation], timeout: 1.0)
}

To make our playground compile we need to create a RessourceError error:

public enum RessourceError: Error {
    case networkError
}

Run the tests -> Red.

Now write the implementation:

public func getTopApps(completion: @escaping ([App], RessourceError?) -> Void) {
    let urlString = "https://itunes.apple.com/fr/rss/toppaidapplications/limit=10/json"

    let url = URL(string: urlString)!

    datafetcher.fetchData(url: url) { data, error in

        guard let _ = data else {
            completion([], .networkError)
            return
        }

        completion([], nil)
    }
}

Run the tests -> Green Remember that each time we just write enough code to make our tests pass.

Now we add another test to make sure everything is wrapped in a "feed" dictionary:

func testGetTopAppsCompletesWithNoFeedError() {
    let getExpectation = self.expectation(description: "getTopApps did not complete with noFeedError")

    // our json string misses the "feed" on purpose
    let jsonString = "{}"
    mockFetcher.mockData = jsonString.data(using: .utf8)
    appStoreRessource.getTopApps { _, error in
        if case let .some(.decodingKeyNotFound(key)) = error, 
        	key.stringValue == "feed" {
                getExpectation.fulfill()
            }    
        }

    self.wait(for: [getExpectation], timeout: 1.0)
}

To make it compile we need to add other cases to our RessourceError enum:

public enum RessourceError: Error {
    case networkError
    case decodingKeyNotFound(key: CodingKey)
    case otherDecodingError
}

Run the tests -> Red

// Struct used for internal Decodable purpose only
private struct ServerResponse: Decodable {
    let feed: Feed
}

private struct Feed: Decodable {}
    
public func getTopApps(completion: @escaping ([App], RessourceError?) -> Void) {
    let urlString = "https://itunes.apple.com/fr/rss/toppaidapplications/limit=10/json"

    let url = URL(string: urlString)!

    datafetcher.fetchData(url: url) { data, _ in

        guard let data = data else {
            completion([], .networkError)
            return
        }

        do {
            _ = try JSONDecoder().decode(ServerResponse.self, from: data)
            completion([], nil)
        } catch DecodingError.keyNotFound(let key, _) {
            completion([], .decodingKeyNotFound(key: key))
        } catch {
            completion([], .otherDecodingError)
        }
    }
}

Run the tests -> Green

Our tests are becoming a little too verbose. Time to refactor with a little helper function:

func getTopAppsHelper(expectationDescription: String, jsonString: String? = nil, completion: @escaping (XCTestExpectation, [App], RessourceError?) -> Void) {
    let getExpectation = self.expectation(description: expectationDescription)

    mockFetcher.mockData = jsonString?.data(using: .utf8)
    appStoreRessource.getTopApps { apps, error in
        completion(getExpectation, apps, error)
    }

    self.wait(for: [getExpectation], timeout: 1.0)
}

Now rewrite the tests with helper function:

func testGetTopAppsCompletes() {
    getTopAppsHelper(expectationDescription: "getTopApps did not complete") { expectation, _, _ in
        expectation.fulfill()
    }
}

func testGetTopAppsCompletesWithNetworkError() {
    // We don't pass any data to the mock on purpose
    getTopAppsHelper(expectationDescription: "getTopApps network error") { expectation, _, error in

        if case .some(.networkError) = error {
            expectation.fulfill()
        }
    }
}

func testGetTopAppsCompletesWithNoFeedError() {
    // our json string misses the "feed" on purpose
    getTopAppsHelper(expectationDescription: "getTopApps noFeedError",
                     jsonString: "{}") { getExpectation, _, error in
        if case let .some(.decodingKeyNotFound(key)) = error, key.stringValue == "feed" {
            getExpectation.fulfill()
        }
    }
}

Run your tests and make sure they are still green.

We now add a test for "entry":

func testGetTopAppsCompletesWithNoEntryError() {
    // our json string misses the "entry" on purpose
    getTopAppsHelper(expectationDescription: "getTopApps noEntryError",
                     jsonString: "{ \"feed\" : {} }") { getExpectation, _, error in
        if case let .some(.decodingKeyNotFound(key)) = error, key.stringValue == "entry" {
            getExpectation.fulfill()
        }
    }
}

Run -> Red

We modify our internal structures for Decodable and allow App to be Decodable:

private struct ServerResponse: Decodable {
    let feed: Feed
}

private struct Feed: Decodable {
    let entry: [App]
}
AppNotDecodable.jpg

We get a compilation error because to be decodable, all your var must also be decodable. So we need to add Decodable conformance to our App Struct (which is still empty by the way):

public struct App {}

extension App: Decodable {}

This should make our test build and pass.

Next we test if we have an error if we don't have an array of Apps:

func testGetTopAppsCompletesWithNoAppsError() {
        // our json string misses the apps array
        getTopAppsHelper(expectationDescription: "getTopApps noAppsError",
                         jsonString: "{ \"feed\" : { \"entry\" : {} } }") { getExpectation, _, error in
                            if case .some(.decodingTypeMismatch) = error {
                                getExpectation.fulfill()
                            }
        }
    }

To make our test compile and pass we add a new error:

public enum RessourceError: Error {
    case networkError
    case decodingKeyNotFound(key: CodingKey)
    case decodingTypeMismatch
    case otherDecodingError
}

and modify our function:

public func getTopApps(completion: @escaping ([App], RessourceError?) -> Void) {
        let urlString = "https://itunes.apple.com/fr/rss/toppaidapplications/limit=10/json"

        let url = URL(string: urlString)!

        datafetcher.fetchData(url: url) { data, _ in

            guard let data = data else {
                completion([], .networkError)
                return
            }

            do {
                let serverResponse = try JSONDecoder().decode(ServerResponse.self, from: data)
                completion(serverResponse.feed.entry, nil)
            } catch DecodingError.keyNotFound(let key, _) {
                completion([], .decodingKeyNotFound(key: key))
            } catch DecodingError.typeMismatch(_, _) {
                completion([], .decodingTypeMismatch)
            } catch {
                print(error)
                completion([], .otherDecodingError)
            }
        }
    }

Run -> Green

So now we need to test the App decoding.

Json for App looks like this:

{
				"im:name": {
					"label": "Toca Hair Salon 3"
				},
				"im:image": [{
						"label": "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/53x53bb-85.png",
						"attributes": {
							"height": "53"
						}
					},
					{
						"label": "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/75x75bb-85.png",
						"attributes": {
							"height": "75"
						}
					},
					{
						"label": "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/100x100bb-85.png",
						"attributes": {
							"height": "100"
						}
					}
				],
				"summary": {
					"label": "Welcome to Toca Hair Salon 3! Our most popular app series returns, better than ever! What do you feel like creating today — something quirky, pretty or edgy? Choose your character from dozens of looks that you can customize — characters have randomized styles and personalities, which creates endless play opportunities! Grab your tools and get styling!\n\nAMAZINGLY REALISTIC HAIR\nThe hair in Toca Hair Salon 3 looks and moves like real hair! You can style silky straight hair, bouncy waves, crinkly curls, and for the first time in the Toca Hair Salon series: kinky hair! This super-curly hair type means you can create fluffy 'fros and other natural hairstyles.\n\nAWESOME STYLING TOOLS\nNo salon would be complete without the right tools!\n\nBasics: Of course you'll find everything for a good wash, including shampoo and a blow dryer. You'll also find scissors, clippers, a razor, a brush and a multipurpose combing tool to use on your client's hair. In case you cut off a little too much, we've brought back our amazing grow tonic to regrow hair — definitely customer pleaser!\n\nHair types: The electric styling tools let you move between the different hair types to get just the right look.\n\nBraids: Use the brand-new braiding tools for more styling options! You can make thick braids or thin braids!\n\nBeards: Visit the beard station for the best in beard-grooming (or growing!). Lather up with shaving cream, then use scissors, clippers or a razor to get just the right length. Every character can have a beard!\n\nColor: Ready for some color? Toca Hair Salon 3 introduces a more advanced hair color tool with two different cans for different sized sprays. And the new rainbow spray lets you create the most colorful styles you can imagine!\n\nCLOTHES AND ACCESSORIES\nNow characters can change into different clothes! Try on new looks to suit that new hair style! Accessorize the look with glasses, caps, headbands and silly stuff. Choose a photo booth background and take a picture to save to your device and share!\n\nFEATURES\n- All-new characters with dozens of starter styles\n- Straight, wavy, curly and now kinky hair!\n- All the tools you need to create the style you want!\n- New braiding tool!\n- Every character can grow a beard to style\n- More advanced hair color tool: Dip-dye and fade hair in any colors you want!\n- Change clothes on characters!\n- Dozens of new accessories: hats, glasses, jewelry and more\n- Change backdrops in the photo booth!\n- Snap a picture in the app and share with your friends!\n- Gender-neutral aesthetic: Unlike most hairstyling apps, all kids are welcome to play!\n- No time limit or high scores — play for as long as you like!\n- No third-party advertising\n\nABOUT TOCA BOCA\nToca Boca is an award winning game studio that makes digital toys for kids. We think playing and having fun is the best way to learn about the world. Therefore we make digital toys and games that help stimulate the imagination, and that you can play together with your kids. Best of all - we do it in a safe way without third-party advertising or in-app purchases.\n\nPRIVACY POLICY\nPrivacy is an issue that we take very seriously. To learn more about how we work with these matters, please read our privacy policy: https://tocaboca.com/privacy"
				},
...

From this json we want to extract:

  • The name of the app
  • The summary of the app
  • A thumbnail image

We can see the structure of the Json is a bit too complex to support out of the box decoding with Decodable. So we need to implement init(from decoder: Decoder).

We add the info to our App struct:

public struct App {
    let name: String
    let summary: String
    let thumbImageUrl: String
}

Our previous test needs some modifications to compile:

App.jpg
class AppTest: XCTestCase {
    func testApp() {
        XCTAssertNotNil(App(name: "name", summary: "summary", thumbImageUrl: "url"))
    }
}

Run -> Green

Let's add a test to Decode the name from the JSON:



class AppTest: XCTestCase {
    let testJson = """
    {
                    "im:name": {
                        "label": "Toca Hair Salon 3"
                    },
                    "im:image": [{
                            "label": "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/53x53bb-85.png",
                            "attributes": {
                                "height": "53"
                            }
                        },
                        {
                            "label": "https://is3-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/75x75bb-85.png",
                            "attributes": {
                                "height": "75"
                            }
                        },
                        {
                            "label": "https://is4-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/100x100bb-85.png",
                            "attributes": {
                                "height": "100"
                            }
                        }
                    ],
                    "summary": {
                        "label": "Welcome to Toca Hair Salon 3! Our most popular app"
    }
    }
    """

    func testApp() {
        XCTAssertNotNil(App(name: "name", summary: "summary", thumbImageUrl: "url"))
    }

    func testDecodableName() {
        let data = testJson.data(using: .utf8)
        let app = try? JSONDecoder().decode(App.self, from: data!)
        XCTAssertEqual(app?.name, "Toca Hair Salon 3")
    }
}

Run -> Red

Write the code to make the test pass:

extension App: Decodable {
    private enum CodingKeys: String, CodingKey {
        case name = "im:name"
    }

    private enum LabelKeys: String, CodingKey {
        case label
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let nameContainer = try container.nestedContainer(keyedBy: LabelKeys.self, forKey: .name)
        name = try nameContainer.decode(String.self, forKey: .label)

        summary = ""
        thumbImageUrl = ""
    }
}

Run -> Green

Now we add a test to decode the summary from the Json:

func testDecodableSummary() {
        let data = testJson.data(using: .utf8)
        let app = try? JSONDecoder().decode(App.self, from: data!)
        XCTAssertEqual(app?.summary, "Welcome to Toca Hair Salon 3! Our most popular app")
    }

Run -> Red

Add the code to make test pass:

extension App: Decodable {
    private enum CodingKeys: String, CodingKey {
        case name = "im:name"
        case summary
    }

    private enum LabelKeys: String, CodingKey {
        case label
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let nameContainer = try container.nestedContainer(keyedBy: LabelKeys.self, forKey: .name)
        name = try nameContainer.decode(String.self, forKey: .label)

        let summaryContainer = try container.nestedContainer(keyedBy: LabelKeys.self, forKey: .summary)
        summary = try summaryContainer.decode(String.self, forKey: .label)
        thumbImageUrl = ""
    }
}

Now we add test for Thumb image url. We want to take the first image url from the array:

func testDecodableThumbImageURL() {
    let data = testJson.data(using: .utf8)
    let app = try? JSONDecoder().decode(App.self, from: data!)
    XCTAssertEqual(app?.thumbImageUrl, "https://is1-ssl.mzstatic.com/image/thumb/Purple128/v4/13/13/aa/1313aa3d-a782-5951-b902-735048917624/AppIcon-1x_U007emarketing-0-85-220-0-8.png/53x53bb-85.png")
}

Run -> Red

Add code to pass test:

extension App: Decodable {
    private enum CodingKeys: String, CodingKey {
        case name = "im:name"
        case summary
        case image = "im:image"
    }

    private enum LabelKeys: String, CodingKey {
        case label
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let nameContainer = try container.nestedContainer(keyedBy: LabelKeys.self, forKey: .name)
        name = try nameContainer.decode(String.self, forKey: .label)

        let summaryContainer = try container.nestedContainer(keyedBy: LabelKeys.self, forKey: .summary)
        summary = try summaryContainer.decode(String.self, forKey: .label)

        var imagesContainer = try container.nestedUnkeyedContainer(forKey: .image)

        var tempImageThumb = ""

        while !imagesContainer.isAtEnd {
            let imageContainer = try imagesContainer.nestedContainer(keyedBy: LabelKeys.self)

            tempImageThumb = try imageContainer.decode(String.self, forKey: LabelKeys.label)
            break
        }

        thumbImageUrl = tempImageThumb
    }
}

Run -> Green

Now all our tests pass and our AppStoreRessource class should be able to download and decode json from Apple feed.

I know at this point our Playground looks like a big mess with Test code and main code in the same file.

Next serie (Part 3) will explain how this playground will be used to create our Classes and Unit tests in the project.

Tagged with: