| # 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<List<Endpoint>> 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<String> observer = (Observer<String>) 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<MediaItem> 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. |