Metronome: A Fully Unit-Tested Rx-Based Reference Application
Several months ago, as a way to get myself up to speed with using and unit-testing RxJava, I developed and posted this reference application. I had ambitions of immediately following it up with a blog post, but I got distracted and I never got around to sharing it with anyone. So better late than never I suppose.
The application is that of a metronome with the following features:
- A core metronome “engine” which triggers “beat” events on a timed interval. The engine exposes methods for starting and stopping the metronome, as well as a method for changing the tempo (i.e. the time between beats).
- A user interface for starting and stopping the metronome, and for making changes to the tempo. The tempo-setting interface is implemented via screen taps. As the use taps an area the screen, the time between the taps is captured and used to set the tempo.
- An audio service that plays a clicking sound on each beat. This is implemented as an Android Service so that the audio can continue playing even if the UI is closed.
Implementation and Testing Details
I won’t cover every aspect of how I implemented the features of the application, but I think it is worth highlighting a few details:
- I used Hugo Visser’s android-apt plugin because I found that the built-in Gradle “provided” functionality didn’t reliably do the apt code-generation for Dagger 2. Dagger 1 worked fine for me without this plug-in, but now that I have transitioned to Dagger 2, android-apt is a must.
- I used Jake Warton’s RxBinding library for observing click events.
- To get the time between screen taps, I used the (very handy) built-in timeInterval Rx operator.
- I wrote a number of Func1 classes for filtering and mapping events. There is a filter that is chained to the events emmitted by timeInterval Rx operator so that only events within a certain range of values are emitted. This restricts the range of tempos that are ultimately set. Also I have a number of mapping classes for doing things like rounding and converting from milliseconds to beats-per-minute (BPM). While these classes are simple and could easily have been provided in-line/anonymously, I chose to break them out into separate classes. This allowed me to easily unit-test each one of them, and makes them available for reuse in another project should I need them.
- I use Dagger 2 to inject all of my dependencies. Most notably, I inject separate @Named Rx Schedulers for subscribing/observing on various threads. Doing so allowed me to override these schedulers in my unit tests with TestScheduler to properly test the behavior of timed and/or asynchronous code without any deferred execution, Thread.sleep(), etc.
- To properly test the Activity and Service classes I used Robolectric. More specifically, I made use of the relatively-new ActivityController and ServiceController classes to control the Activity/Service lifecyles. This allowed me to have these classes get properly initialized and Dagger-injected (in their respective onCreate methods), but still able to override their dependencies as needed before the subsequent lifecycle events such as onStart were called.
- I used Mockito to generate mocked dependencies and AssertJ for writing assertions, because… well… they’re great.
- (Edit Jan 19, 2016) Thanks to a pull request from R. Toledo, Jacoco test-coverage reports are now available via the jacocoTestReport Gradle task.
The source code of the project is available here: https://github.com/wongcain/metronome-android/
Also, here is a list of the tools/libraries that I used:
- Dagger 2: http://google.github.io/dagger/
- Android-Apt: https://bitbucket.org/hvisser/android-apt
- RxJava: https://github.com/ReactiveX/RxJava
- RxAndroid: https://github.com/ReactiveX/RxAndroid
- RxBinding: https://github.com/JakeWharton/RxBinding
- Butterknife: http://jakewharton.github.io/butterknife/
- Robolectric: http://robolectric.org/
- Mockito: http://site.mockito.org/
- AssertJ Android: http://square.github.io/assertj-android/
- Jacoco: http://eclemma.org/jacoco/
I hope that this post and the sample application are helpful. Feel free to ask me any questions or offer any suggestions. Cheers!