Многие приложения используют Hilt для внедрения различного поведения в разные варианты сборки. Это может быть особенно полезно при микробенчмаркинге вашего приложения, поскольку позволяет исключить компонент, который может исказить результаты. Например, в следующем фрагменте кода показан репозиторий, который извлекает и сортирует список имен:
Котлин
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 })) ) } } }}
Ява
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, и для сравнительного тестирования вы можете заменить его следующей поддельной реализацией:
Котлин
class FakeNetworkDataSource @Inject constructor( private val people: List<Person> ) : NetworkDataSource { override fun getPeople(): List<Person> = people }
Ява
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 мог это внедрить, используется следующий провайдер:
Котлин
@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) } }
Ява
@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
модуля, содержащего поддельную реализацию, выглядит следующим образом:
Котлин
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, следующим образом:
Котлин
class HiltBenchmarkRunner : AndroidBenchmarkRunner() { override fun newApplication( cl: ClassLoader?, className: String?, context: Context? ): Application { return super.newApplication(cl, HiltTestApplication::class.java.name, context) } }
Ява
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
. Внесите следующие изменения в конфигурацию сборки:
Котлин
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
, которая выполняет тестирование производительности.
Создайте микробенчмарк
Бенчмарк реализован следующим образом:
Котлин
@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() } } }
Ява
@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, вы можете вместо этого использовать его для внедрения подделок для сравнительного анализа.