Skip to content

 Sample iOS App - A collection of examples and patterns for Unit Testing, UI Testing, handling Result/Optionals, writing documentation, and more. Details in README.

License

Notifications You must be signed in to change notification settings

nsoojin/BookStore-iOS

Repository files navigation

BookStore

See new releases and search for programming books from IT Bookstore API

This is a sample app to practice using Result type, stubbing network request for unit tests, separating functionalities into frameworks, and writing Swift documentation.

How to run

> cd BookStore
> open BookStore.xcodeproj

Run!

Contents

App Features

What's New

A simple UITableView with cells and modal presentation for a detailed page.

Search

  1. As a user types in the keyword, the search text is "debounced" for a fraction of second for better performance and user experience. See Debouncer.

  2. Search results are paginated and provides infinite scroll.

Result type in Swift 5

Out of the box, you have to switch on the Result instance to access the underlying success instance or the error instance.

switch result {
case .success(let response):
  //do something with the response
case .failure(let error):
  //handle error
}

However, I think switch statements are too wordy. I added success and catch method to Result type. So it can be chained like this.

searchResult.success { response in
  //do something with the response
}.catch { error in
  //handle error
}

Even cleaner, like this.

result.success(handleSuccess)
      .catch(handleError)
      
func handleSuccess(_ result: SearchResult) { ... }
func handleError(_ error: Error) { ... }

Stubbing Network Requests for Unit Tests

Generally, it is not a good idea to rely on the actual network requests for unit tests because it adds too much dependency on tests. One way to stub networking is to subclass URLProtocol.

1. Subclass URLProtocol

See MockURLProtocol

2. Configure URLSession with your mock URLProtocol

let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]

//Use this URLSession instance to make requests.
let session = URLSession(configuration: config) 

4. Use the configured URLSession instance just as you would.

session.dataTask(with: urlRequest) { (data, response, error) in
  //Stubbed response
}.resume()

UI Testing with Stubbed Network Data

The above method(as well as the famous OHHTTPStubs) doesn't work for UI testing because the test bundle and the app bundle (XCUIApplication) are loaded in separate processes. By using Swifter, you can run a local http server on the simulator.

First, change the API endpoints during UI testing with launchArguments in your hosting app.

//In XCTestCase,
override func setUp() {
  app = XCUIApplication()
  app.launchArguments = ["-uitesting"]
}

//In AppDelegate's application(_:didFinishLaunchingWithOptions:)
if ProcessInfo.processInfo.arguments.contains("-uitesting") {
  BookStoreConfiguration.shared.setBaseURL(URL(string: "http://localhost:8080")!)
}

Then stub the network and test the UI with it.

let server = HttpServer()

func testNewBooksNormal() {
  do {
    let path = try TestUtil.path(for: normalResponseJSONFilename, in: type(of: self))
    server[newBooksPath] = shareFile(path)
    try server.start()
    app.launch()
  } catch {
    XCTAssert(false, "Swifter Server failed to start.")
  }
        
  XCTContext.runActivity(named: "Test Successful TableView Screen") { _ in
    XCTAssert(app.tables[tableViewIdentifier].waitForExistence(timeout: 3))
    XCTAssert(app.tables[tableViewIdentifier].cells.count > 0)
    XCTAssert(app.staticTexts["9781788476249"].exists)
    XCTAssert(app.staticTexts["$44.99"].exists)
  }
}

Using Frameworks for independent functionalities

Separating your app's functions into targets has several advantages. It forces you to care about dependencies, and it is good for unit tests since features are sandboxed. However, it may slow down the app launch (by little) due to framework loading.

BookStoreKit is responsible for fetching and searching books data from IT Bookstore API.

Networking is a wrapper around URLSession for making HTTP requests and parsing response.

Writing a documentation comment

Swift's API Design Guidelines suggest you write a documentation comment for every declaration. Writing one can have an impact on the design.

1. Write

Reference this document for markup formatting.

2. Check out the result

In Xcode's autocompletion

and Show Quick Help (option + click)

Getting Rid of IUOs

IMHO Implictly unwrapped optional is a potential threat to code safety and should be avoided as much as possible if not altogether. An example of two methods to get rid of them from where they are commonly used.

Make IBOutlets Optional

IBOutlets are IUOs by Apple's default. However, you can change that to Optional types. You may worry that making IBOutlets Optionals may cause too many if lets or guards, but that concern may just be overrated. IBOutlets are mostly used to set values on them, so optional chaining is sufficient. In just few cases where unwrapping is added, I will embrace them for additional safety of my code.

Using lazy instantiation

For the properties of UIViewController subclass, IUO can be useful but it's still dangerous. Instead, I use unspecified. It generates a crash upon class/struct usage so it can be spotted fast during development, and most importantly no more IUOs.

//Inside a viewcontroller
lazy var bookStore: BookStoreService = unspecified()