Demo iOS app showcasing BDD, TDD, reactive UI and OOP design patterns
City Search is a simple Android app made to showcase various concepts of Xtreme Programming, including test-driven development (TDD) and behavior-driven development (BDD), and object-oriented design patterns, including unidirectional data, reactive programming, reactive UI, dependency injection, creational and behavioral patterns, and separation of interfaces and implementations. It is written entirely in Kotlin.
The features begin with an animated title screen. Once the data is ready, the app transitions to a "main" page that displays a horizontally scrolling grid of cities in front of a parallax-scrolling cityscape. Each item may be tapped to navigate to a details screen, which presents the city's name, population, its location on a map of Earth, and a horizontally scrolling list of images found on the web for that city.
These features showcase the common fundamental behavior of a mobile application: a dataset in the form of a sequence, presented to the user to select one in particular, and then present more details for the selected item. The data is retrieved through calls to a RESTful web service.
What is really being showcased here is the code design. The GUI components are structured by the model-view-viewmodel (MVVM) design pattern suite, which includes the Observable pattern, Mediator pattern, the Composite pattern, and Command pattern. The Model defines and manages the data and actions for a page or element, which makes up the business logic. The Model exposes a set of observable properties that represent the current state of whatever is being presented to the user. The ViewModel defines and manages the visual state for a page of element, which makes up the presentation logic. The ViewModel transforms the Model's state, which is generally not in a visually presentable format, into a set of visually presentable data, such as text, images, etc. The View manages a composite hierarchy of objects responsible for drawing the ViewModel's state on the screen, and is primarily expressed in the form of XML layouts. The View consumes the ViewModel's state and makes it visible to the user.
The MVVM components are hierarchical. A page's Model is composed of the Models for elements within that page, which in turn are composed of Models for its elements, and so on. The same is true of the ViewModels and Views. The separation of concerns is carried down to the level of individual widgets, such as TextViews and ImageViews.
The Observable pattern is implemented in its state-of-the-art form through Reactive Extensions. The goal is to express as much of the business and presentation logic as possible declaratively, as a relationship among various streams of events. The flow of data through the MVVM components is unidirectional. Changes to the state always originate in the Model. The event of a Model's state change is publicly available through the observability of that state. There are two primarily observers of a Model: its associated ViewModel, and other Models. The associated ViewModel declares its presentation state as a function of the Model's state: visualState = function(modelState). By using Reactive extensions, this functional relationship can be neatly expressed in a way that exposes the output of the function (the visual state) as another observable that can then be consumed by the View. Models also observe each other. One Model's state may be a function of other Models' states, such as an aggregation of its child Models' states. This is generally how events propagate around the app. A notable example here is in the Model for the details screen. This Model drives its child Model, the Async Image Carousel, by taking an observable mapped from its image search service, and passing it to the carousel model as the results. The Model also uses this observable to define its own "loading" state. An example of this collaboration for visual state is the SearchViewModel. It connects the SearchResultsViewModel's content offset observable to the ParallaxViewModel, thereby causing the Parallax View to scroll along with the search results collection.
Just as important, if not more important, are the tests. The tests are organized into two types: acceptance tests and unit tests. Not only do the tests provide automated regression coverage, they drove the design of the app. All the tests are written in Gherkin syntax, in order to express the behavior being tested. This is behavior-driven development (BDD). The first step of adding a feature is to define the behavior of the feature. Expressed in Gherkin, this behavioral specification becomes the acceptance test for the feature, which fails because the feature isn't implemented yet. The test is implemented by providing the step definitions. The steps are written as if code that doesn't exist yet is already written. They will make reference to classes and methods that aren't there. These will appear as red in the IDE and the test won't compile. The next step is to create the classes/methods with stubbed implementations. This makes the test compile, and fail when it runs. Then, for each newly created class/method, a unit test is written to specify the behavior it will contribute to the feature's behavior. In this step, we begin thinking about how the behavior is implemented. We realize this class may need another class, a dependency, so we again write the steps as if these dependent classes already exist. We create their definitions (typically as interfaces), then mock their behavior. The test runs and fails. We then implement the class being tested to make the test pass. Then we repeat this process with all the classes/methods that were defined and mocked. The mocked behavior (the "given" of the Gherkin) becomes the expected behavior (the "then" of the Gherkin) of the next test. This process of "write test, discover dependencies, mock dependencies, make the test pass" continues until we reach classes "low level" enough that they have no dependencies. We then go back to the acceptance test and see if it passes. If it doesn't, there is more stubbed out behavior (or an error in the unit design), so we repeat the process with any remaining stubbed behavior. Eventually, all behavior is implemented, and the acceptance test passes. Then the feature is complete, and we repeat the process with the next feature.
(Note that if you examine the commit history of this project, you'll see the tests were written after the production code. The Android version of this app is a port of the iOS version, whose commit history shows the BDD/TDD process being followed)