# Testability [TOC] ## How to write testable libraries When developers use a Jetpack library, it should be easy to write reliable and automated tests for their own code's functionality. In most cases, tests written against a library will need to interact with that library in some way -- setting up preconditions, reaching synchronization points, making calls -- and the library should provide necessary functionality to enable such tests, either through public APIs or optional `-testing` artifacts. **Testability**, in this document, means how easily and effectively the users of a library can create tests for apps that use that library. NOTE Tests that check the behavior of a library have a different mission than tests made by app developers using that library; however, library developers may find themselves in a similar situation when writing tests for sample code. Often, the specifics of testability will vary from library to library and there is no one way of providing it. Some libraries have enough public API surface, others provide additional testing artifacts (e.g. [Lifecycle Runtime Testing artifact](https://maven.google.com/web/index.html?q=lifecycle#androidx.lifecycle:lifecycle-runtime-testing)). The best way to check if your library is testable is to try to write a sample app with tests. Unlike regular library tests, these apps will be limited to the public API surface of the library. Keep in mind that a good test for a sample app should have: * [No side effects](#side-effects) * [No dependencies on time / looper (except for UI)](#external-dependencies) * [No private API calls](#private-apis) * [No assumptions on undefined library behavior](#undefined-behavior) If you are able to write such tests for your library, you are good to go. If you struggled or found yourself writing duplicate testing code, there is room for improvement. To get started with sample code, see [Sample code in Kotlin modules](/docs/api_guidelines/index.md#sample-code-in-kotlin-modules) for information on writing samples that can be referenced from API reference documentation or [Project directory structure](/docs/api_guidelines/index.md#module-structure) for module naming guidelines if you'd like to create a basic test app. ### Avoiding side-effects {#side-effects} #### Ensure proper scoping for your library a.k.a. Avoid Singletons Singletons are usually bad for tests as they live across different tests, opening the gates for possible side-effects between tests. When possible, try to avoid using singletons. If it is not possible, consider providing a test artifact that will reset the state of the singleton between tests. ```java {.bad} public class JobQueue { public static JobQueue getInstance(); } ``` ```java {.good} public class JobQueue { public JobQueue(); } ``` ```kotlin {.good} object JobQueueTestUtil { fun createForTest(): JobQueue fun resetForTesting(jobQueue: JobQueue) } ``` #### Side effects due to external resources Sometimes, your library might be controlling resources on the device in which case even if it is not a singleton, it might have side-effects. For instance, Room, being a database library, inherently modifies a file on the disk. To allow proper isolated testing, Room provides a builder option to create the database [in memory](https://developer.android.com/reference/androidx/room/Room#inMemoryDatabaseBuilder\(android.content.Context,%20java.lang.Class%3CT%3E\)) A possible alternative solution could be to provide a test rule that will properly close databases after each test. ```java {.good} public class Camera { // Sends configuration to the camera hardware, which persists the // config across app restarts and applies to all camera clients. public void setConfiguration(Config c); // Retrieves the current configuration, which allows clients to // restore the camera hardware to its prior state after testing. public Config getConfiguration(); } ``` If your library needs an inherently singleton resource (e.g. `WorkManager` is a wrapper around `JobScheduler` and there is only 1 instance of it provided by the system), consider providing a testing artifact. To provide isolation for tests, the WorkManager library ships a [separate testing artifact](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing) ### "External" dependencies {#external-dependencies} #### Allow configuration of external resource dependencies A common example of this use case is libraries that do multi-threaded operations. For Kotlin libraries, this is usually achieved by receiving a coroutine context or scope. For Java libraries, it is commonly an `Executor`. If you have a case like this, make sure it can be passed as a parameter to your library. NOTE Android API Guidelines require that methods accepting a callback [must also take an Executor](https://android.googlesource.com/platform/developers/docs/+/refs/heads/master/api-guidelines/index.md#provide-executor) For example, the Room library allows developers to [pass different executors](https://developer.android.com/reference/androidx/room/RoomDatabase.Builder#setQueryExecutor\(java.util.concurrent.Executor\)) for background query operations. When writing a test, developers can invoke this with a custom executor where they can track work completion. See [SuspendingQueryTest](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt;l=672) in Room's integration test app for implementation details. ```kotlin val localDatabase = Room.inMemoryDatabaseBuilder( ApplicationProvider.getApplicationContext(), TestDatabase::class.java ) .setQueryExecutor(ArchTaskExecutor.getIOThreadExecutor()) .setTransactionExecutor(wrappedExecutor) .build() // ... wrappedExecutor.awaitTermination(1, TimeUnit.SECONDS) ``` * [sample test](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-master-dev:room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt;l=672) If the external resource you require does not make sense as a public API, such as a main thread executor, then you can provide a testing artifact which will allow setting it. For example, the Lifecycle package depends on the main thread executor to function but for an application, customizing it does not make sense (as there is only 1 "pre-defined" main thread for an app). For testing purposes, the Lifecycle library provides a testing artifact which includes the [CountingTaskExecutorRule](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:arch/core/core-testing/src/main/java/androidx/arch/core/executor/testing/CountingTaskExecutorRule.java;l=36) JUnit test rule to change them. ```kotlin @Rule @JvmField val countingTaskExecutorRule = CountingTaskExecutorRule() // ... @After fun teardown() { // At the end of all tests, query executor should // be idle (e.g. transaction thread released). countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS) assertThat(countingTaskExecutorRule.isIdle).isTrue() } ``` #### Fakes for external dependencies Sometimes, the developer might want to track side effects of your library for end-to-end testing. For instance, if your library provides some functionality that might decide to toggle Bluetooth -- outside developer's direct control -- it might be a good idea to have an interface for that functionality and also provide a fake that can record such calls. If you don't think that interface makes sense as a library configuration, you can use the [@RestrictTo](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:annotation/annotation/src/main/java/androidx/annotation/RestrictTo.java) annotation with scope [LIBRARY_GROUP](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:annotation/annotation/src/main/java/androidx/annotation/RestrictTo.java;l=69) to restrict usage of that interface to your library group and provide a testing artifact with the fake so that developer can observe side effects only in tests while you can avoid creating unnecessary APIs. ```kotlin public class EndpointConnector { public void discoverEndpoints(Executor e, Consumer> callback); @RestrictTo(Scope.LIBRARY_GROUP) public void setBleInterface(BleInterface bleInterface); } public class EndpointConnectorTestHelper { public void setBleInterface(EndpointConnector e, BleInterface b); } ``` NOTE There is a fine balance between making a library configurable and making configuration a nightmare for the developer. You should try to always have defaults for these configurable objects to ensure your library is easy to use while still testable when necessary. ### Avoiding the need for private API calls in tests {#private-apis} #### Provide additional functionality for tests There are certain situations where it could be useful to provide more APIs that only make sense in the scope of testing. For example, a `Lifecycle` class has methods that are bound to the main thread but developers may want to have other tests that rely on lifecycle but do not run on the main thread (or even on an emulator). For such cases, you may create APIs that are testing-only to allow developers to use them only in tests while giving them the confidence that it will behave as close as possible to a real implementation. For the case above, `LifecycleRegistry` provides an API to [create](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/LifecycleRegistry.java;l=334) an instance of it that will not enforce thread restrictions. NOTE Even though the implementation referenced above is acceptable, it is always better to create such functionality in additional testing artifacts when possible. When writing Android platform APIs, testing-only APIs should be clearly distinguished from non-test API surface and restricted as necessary to prevent misuse. In some cases, the `@TestApi` annotation may be appropriate to restrict usage to CTS tests; however, many platform testing APIs are also useful for app developers. ```java {.good} class AdSelectionManager { /** * Returns testing-specific APIs for this manager. * * @throws SecurityException when called from a non-debuggable application */ public TestAdSelectionManager getTestAdSelectionManager(); } ``` ### Avoiding assumptions in app code for library behavior {#undefined-behavior} #### Provide fakes for common classes in a `-testing` artifact In some cases, the developer might need an instance of a class provided by your library but does not want to (or cannot) initiate it in tests. Moreover, behavior of such classes might not be fully defined for edge cases, making it difficult for developers to mock them. For such cases, it is a good practice to provide a fake implementation out of the box that can be controlled in tests. For example, the Lifecycle library provides [TestLifecycleOwner](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-runtime-testing/src/main/java/androidx/lifecycle/testing/TestLifecycleOwner.kt) as a fake implementation for the `LifecycleOwner` class that can be manipulated in tests to create different use cases. ```java private TestLifecycleOwner mOwner = new TestLifecycleOwner( Lifecycle.State.INITIALIZED, new TestCoroutineDispatcher()); @Test public void testObserverToggle() { Observer observer = (Observer) mock(Observer.class); mLiveData.observe(mOwner, observer); verify(mActiveObserversChanged, never()).onCall(anyBoolean()); // ... } ``` ## Document how to test with your library Even when your library is fully testable, it is often not clear for a developer to know which APIs are safe to call in tests and when. Providing guidance on [d.android.com](https://d.android.com) or in a [Medium post](https://medium.com/androiddevelopers) will make it much easier for the developer to start testing with your library. Examples of testing guidance: - [Integration tests with WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/integration-testing) - [Test and debug your Room database](https://developer.android.com/training/data-storage/room/testing-db) - [Unit-testing LiveData and other common observability problems](https://medium.com/androiddevelopers/unit-testing-livedata-and-other-common-observability-problems-bb477262eb04) ## Testability anti-patterns When writing integration tests against your code that depends on your library, look out for the following anti-patterns. ### Calling `Instrumentation.waitForIdleSync()` as a synchronization barrier The `waitForIdle()` and `waitForIdleSync()` methods claim to "(Synchronously) wait for the application to be idle." and may seem like reasonable options when there is no obvious way to observe whether an action has completed; however, these methods know nothing about the context of the test and return when the main thread's message queue is empty. ```java {.bad} view.requestKeyboardFocus(); Instrumentation.waitForIdleSync(); sendKeyEvents(view, "1234"); // There is no guarantee that `view` has keyboard focus yet. device.pressEnter(); ``` In apps with an active UI animation, the message queue is *never empty*. If the app is waiting for a callback across IPC, the message queue may be empty despite the test not reaching the desired state. In some cases, `waitForIdleSync()` introduces enough of a delay that unrelated asynchronous actions happen to have completed by the time the method returns; however, this delay is purely coincidental and eventually leads to flakiness. Instead, find a reliable synchronization barrier that guarantees the expected state has been reached or the requested action has been completed. This might mean adding listeners, callbacks, `ListenableFuture`s, or `LiveData`. See [Asynchronous work](/docs/api_guidelines/index.md#async) in the API Guidelines for more information on exposing the state of asynchronous work to clients. ### Calling `Thread.sleep()` as a synchronization barrier `Thread.sleep()` is a common source of flakiness and instability in tests. If a developer needs to call `Thread.sleep()` -- even indirectly via a `PollingCheck` -- to get their test into a suitable state for checking assertions, your library needs to provide more reliable synchronization barriers. ```java {.bad} List playlist = MediaTestUtils.createPlaylist(playlistSize); mPlayer.setPlaylist(playlist); // Wait some time for setting the playlist. Thread.sleep(TIMEOUT_MS); assertTrue(mPlayer.getPositionInPlaylist(), 0); ``` See the previous header for more information of providing synchronization barriers.