Многие приложения используют Hilt, чтобы внедрять разные модели поведения в разные варианты сборки. Это может быть особенно полезно при микротестировании приложения, поскольку позволяет заменить компонент, который может исказить результаты. Например, в следующем фрагменте кода показан репозиторий, который извлекает и сортирует список имен:
Kotlin
class PeopleRepository @Inject constructor( @Kotlin private val dataSource: NetworkDataSource, @Dispatcher(DispatcherEnum.IO) private val dispatcher: CoroutineDispatcher ) { private val _peopleLiveData = MutableLiveData<List<Person>>() val peopleLiveData: LiveData<List<Person>> get() = _peopleLiveData suspend fun update() { withContext(dispatcher) { _peopleLiveData.postValue( dataSource.getPeople() .sortedWith(compareBy({ it.lastName }, { it.firstName })) ) } } }}
Java
public class PeopleRepository { private final MutableLiveData<List<Person>> peopleLiveData = new MutableLiveData<>(); private final NetworkDataSource dataSource; public LiveData<List<Person>> getPeopleLiveData() { return peopleLiveData; } @Inject public PeopleRepository(NetworkDataSource dataSource) { this.dataSource = dataSource; } private final Comparator<Person> comparator = Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName); public void update() { Runnable task = new Runnable() { @Override public void run() { peopleLiveData.postValue( dataSource.getPeople() .stream() .sorted(comparator) .collect(Collectors.toList()) ); } }; new Thread(task).start(); } }
Если вы включаете сетевой вызов при тестировании, реализуйте поддельный сетевой вызов, чтобы получить более точный результат.
Если при тестировании производительности выполняется реальный сетевой вызов, интерпретировать результаты сложнее. На сетевые вызовы может влиять множество внешних факторов, и их продолжительность может варьироваться от итерации к итерации. Вызовы по сети могут занимать больше времени, чем сортировка.
Реализуйте поддельный сетевой вызов с помощью Hilt
Вызов dataSource.getPeople(), как показано в предыдущем примере, содержит сетевой вызов. Однако экземпляр NetworkDataSource внедряется с помощью Hilt, и вы можете заменить его следующей фиктивной реализацией для тестирования:
Kotlin
class FakeNetworkDataSource @Inject constructor( private val people: List<Person> ) : NetworkDataSource { override fun getPeople(): List<Person> = people }
Java
public class FakeNetworkDataSource implements NetworkDataSource{ private List<Person> people; @Inject public FakeNetworkDataSource(List<Person> people) { this.people = people; } @Override public List<Person> getPeople() { return people; } }
Этот фиктивный сетевой вызов предназначен для максимально быстрого выполнения при вызове метода getPeople(). Чтобы Hilt мог внедрить это, используется следующий поставщик:
Kotlin
@Module @InstallIn(SingletonComponent::class) object FakekNetworkModule { @Provides @Kotlin fun provideNetworkDataSource(@ApplicationContext context: Context): NetworkDataSource { val data = context.assets.open("fakedata.json").use { inputStream -> val bytes = ByteArray(inputStream.available()) inputStream.read(bytes) val gson = Gson() val type: Type = object : TypeToken<List<Person>>() {}.type gson.fromJson<List<Person>>(String(bytes), type) } return FakeNetworkDataSource(data) } }
Java
@Module @InstallIn(SingletonComponent.class) public class FakeNetworkModule { @Provides @Java NetworkDataSource provideNetworkDataSource( @ApplicationContext Context context ) { List<Person> data = new ArrayList<>(); try (InputStream inputStream = context.getAssets().open("fakedata.json")) { int size = inputStream.available(); byte[] bytes = new byte[size]; if (inputStream.read(bytes) == size) { Gson gson = new Gson(); Type type = new TypeToken<ArrayList<Person>>() { }.getType(); data = gson.fromJson(new String(bytes), type); } } catch (IOException e) { // Do something } return new FakeNetworkDataSource(data); } }
Данные загружаются из объектов с помощью вызова ввода-вывода потенциально переменной длины.
Однако это происходит во время инициализации и не вызывает никаких нарушений при вызове getPeople() во время тестирования.
В некоторых приложениях уже используются поддельные объекты в отладочных сборках, чтобы удалить все зависимости от бэкенда. Однако для тестирования нужно использовать сборку, максимально близкую к конечной сборке. В остальной части этого документа используется структура с несколькими модулями и вариантами, как описано в разделе Полная настройка проекта.
В этом курсе три модуля:
benchmarkable– содержит код для тестирования.benchmark– содержит код контрольного показателя.app– содержит остальной код приложения.
У каждого из перечисленных выше модулей есть вариант сборки benchmark, а также обычные варианты debug и release.
Как настроить модуль контрольных показателей
Код для имитации сетевого вызова находится в наборе исходных файлов debug модуля benchmarkable, а полная реализация сети – в наборе исходных файлов release того же модуля. Файл объекта, содержащий данные, возвращаемые поддельной реализацией, находится в наборе исходных файлов debug, чтобы избежать раздувания APK в сборке release. Вариант benchmark должен быть основан на release и использовать набор исходных файлов debug. Конфигурация сборки для варианта benchmark модуля benchmarkable, содержащего поддельную реализацию, выглядит следующим образом:
Kotlin
android { ... buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } create("benchmark") { initWith(getByName("release")) } } ... sourceSets { getByName("benchmark") { java.setSrcDirs(listOf("src/debug/java")) assets.setSrcDirs(listOf("src/debug/assets")) } } }
Яркий
android { ... buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ) } benchmark { initWith release } } ... sourceSets { benchmark { java.setSrcDirs ['src/debug/java'] assets.setSrcDirs(listOf ['src/debug/assets'] } } }
В модуле benchmark добавьте пользовательский модуль запуска тестов, который создает Application для запуска тестов с поддержкой Hilt, как показано ниже.
Kotlin
class HiltBenchmarkRunner : AndroidBenchmarkRunner() { override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
Java
public class JavaHiltBenchmarkRunner extends AndroidBenchmarkRunner { @Override public Application newApplication( ClassLoader cl, String className, Context context ) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication(cl, HiltTestApplication.class.getName(), context); } }
В результате объект Application, в котором выполняются тесты, расширяет класс HiltTestApplication. Внесите следующие изменения в конфигурацию сборки:
Kotlin
plugins { alias(libs.plugins.android.library) alias(libs.plugins.benchmark) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kapt) alias(libs.plugins.hilt) } android { namespace = "com.example.hiltmicrobenchmark.benchmark" compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner = "com.example.hiltbenchmark.HiltBenchmarkRunner" } testBuildType = "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "benchmark-proguard-rules.pro" ) } create("benchmark") { initWith(getByName("debug")) } } } dependencies { androidTestImplementation(libs.bundles.hilt) androidTestImplementation(project(":benchmarkable")) implementation(libs.androidx.runner) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.junit) implementation(libs.androidx.benchmark) implementation(libs.google.dagger.hiltTesting) kaptAndroidTest(libs.google.dagger.hiltCompiler) androidTestAnnotationProcessor(libs.google.dagger.hiltCompiler) }
Яркий
plugins { alias libs.plugins.android.library alias libs.plugins.benchmark alias libs.plugins.jetbrains.kotlin.android alias libs.plugins.kapt alias libs.plugins.hilt } android { namespace = 'com.example.hiltmicrobenchmark.benchmark' compileSdk = 34 defaultConfig { minSdk = 24 testInstrumentationRunner 'com.example.hiltbenchmark.HiltBenchmarkRunner' } testBuildType "benchmark" buildTypes { debug { // Since isDebuggable can't be modified by Gradle for library modules, // it must be done in a manifest. See src/androidTest/AndroidManifest.xml. minifyEnabled true proguardFiles( getDefaultProguardFile('proguard-android-optimize.txt'), 'benchmark-proguard-rules.pro' ) } benchmark { initWith debug" } } } dependencies { androidTestImplementation libs.bundles.hilt androidTestImplementation project(':benchmarkable') implementation libs.androidx.runner androidTestImplementation libs.androidx.junit androidTestImplementation libs.junit implementation libs.androidx.benchmark implementation libs.google.dagger.hiltTesting kaptAndroidTest libs.google.dagger.hiltCompiler androidTestAnnotationProcessor libs.google.dagger.hiltCompiler }
В приведенном выше примере выполняются следующие действия:
- Применяет к сборке необходимые плагины Gradle.
- Указывает, что для выполнения тестов используется пользовательский исполнитель тестов.
- Указывает, что вариант
benchmarkявляется типом теста для этого модуля. - Добавляет вариант
benchmark. - Добавляет необходимые зависимости.
Вам нужно изменить testBuildType, чтобы Gradle создал задачу connectedBenchmarkAndroidTest, которая выполняет тестирование.
Создайте микротест
Контрольный показатель рассчитывается следующим образом:
Kotlin
@RunWith(AndroidJUnit4::class) @HiltAndroidTest class PeopleRepositoryBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @get:Rule val hiltRule = HiltAndroidRule(this) private val latch = CountdownLatch(1) @Inject lateinit var peopleRepository: PeopleRepository @Before fun setup() { hiltRule.inject() } @Test fun benchmarkSort() { benchmarkRule.measureRepeated { runBlocking { benchmarkRule.getStart().pauseTiming() withContext(Dispatchers.Main.immediate) { peopleRepository.peopleLiveData.observeForever(observer) } benchmarkRule.getStart().resumeTiming() peopleRepository.update() latch.await() assert(peopleRepository.peopleLiveData.value?.isNotEmpty() ?: false) } } } private val observer: Observer<List<Person>> = object : Observer<List<Person>> { override fun onChanged(people: List<Person>?) { peopleRepository.peopleLiveData.removeObserver(this) latch.countDown() } } }
Java
@RunWith(AndroidJUnit4.class) @HiltAndroidTest public class PeopleRepositoryBenchmark { @Rule public BenchmarkRule benchmarkRule = new BenchmarkRule(); @Rule public HiltAndroidRule hiltRule = new HiltAndroidRule(this); private CountdownLatch latch = new CountdownLatch(1); @Inject JavaPeopleRepository peopleRepository; @Before public void setup() { hiltRule.inject(); } @Test public void benchmarkSort() { BenchmarkRuleKt.measureRepeated(benchmarkRule, (Function1<BenchmarkRule.Scope, Unit>) scope -> { benchmarkRule.getState().pauseTiming(); new Handler(Looper.getMainLooper()).post(() -> { awaitValue(peopleRepository.getPeopleLiveData()); }); benchmarkRule.getState().resumeTiming(); peopleRepository.update(); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } assert (!peopleRepository.getPeopleLiveData().getValue().isEmpty()); return Unit.INSTANCE; }); } private <T> void awaitValue(LiveData<T> liveData) { Observer<T> observer = new Observer<T>() { @Override public void onChanged(T t) { liveData.removeObserver(this); latch.countDown(); } }; liveData.observeForever(observer); return; } }
В примере выше создаются правила и для теста производительности, и для Hilt.
benchmarkRule выполняет хронометраж теста. hiltRule выполняет внедрение зависимостей в тестовый класс. Чтобы выполнить внедрение перед запуском отдельных тестов, необходимо вызвать метод inject() правила Hilt в функции @Before.
Сам тест производительности приостанавливает отсчет времени, пока зарегистрирован наблюдатель LiveData. Затем он использует защелку, чтобы дождаться обновления LiveData перед завершением. Поскольку сортировка выполняется между вызовом peopleRepository.update() и получением обновления LiveData, ее продолжительность включается в контрольное время.
Запустите микротест
Запустите тест с помощью ./gradlew :benchmark:connectedBenchmarkAndroidTest, чтобы выполнить его несколько раз и вывести данные о времени в Logcat:
PeopleRepositoryBenchmark.log[Metric (timeNs) results: median 613408.3952380952, min 451949.30476190476, max 1412143.5142857144, standardDeviation: 273221.2328680522...
В предыдущем примере показан результат теста от 0,6 мс до 1,4 мс для запуска алгоритма сортировки в списке из 1000 элементов. Однако если вы включите сетевой вызов в тест, то разница между итерациями будет больше, чем время, необходимое для выполнения самой сортировки, отсюда необходимость изолировать сортировку от сетевого вызова.
Вы всегда можете провести рефакторинг кода, чтобы упростить запуск сортировки в изоляции, но если вы уже используете Hilt, то можете использовать его для внедрения подделок для тестирования производительности.