- BrainTools - https://www.braintools.ru -
На российском рынке искусственного интеллекта [1] произошло событие, мимо которого сложно пройти даже самому заядлому скептику — T-Банк представил свои языковые модели T-Lite и T-Pro, основанные на китайской LLM Qwen 2.5. И хотя анонсов «революционных» нейросетей в последнее время становится всё больше, этот случай действительно заслуживает пристального внимания [2] — перед нами не очередной наспех слепленный форк с громкими заявлениями, а результат полугодовой работы над полноценным решением с открытой лицензией Apache 2.0.
T-Банк представил две модели разного масштаба: T-Lite на 7 миллиардов параметров и T-Pro на 32 миллиарда параметров. Обе модели построены на базе Qwen 2.5 и прошли серьёзное дообучение для работы с русским языком. Особенно интересен сам процесс их создания — команда T-Банка использовала многоступенчатый подход к обучению [3]:
Первичный претрейн на 100B токенов русскоязычных данных из Common Crawl, книг, кода и проприетарных датасетов
Вторичный претрейн на 40B токенов с фокусом на инструктивные данные
SFT (Supervised Fine-Tuning) на 1B токенов для улучшения следования инструкциям
Финальная настройка предпочтений также на 1B токенов
Такой подход позволил создать модели, которые не просто понимают русский язык, но и способны эффективно работать в различных доменах — от написания кода до ведения диалогов. По заявлению разработчиков, T-Lite стала лучшей русскоязычной опенсорс-моделью в классе до 10 млрд параметров, а T-Pro показывает впечатляющие результаты в сравнении даже с более крупными моделями.
Обе модели работают с контекстным окном в 8k токенов, хотя базовая модель Qwen 2.5 поддерживает до 32k. Команда сохранила оригинальный токенизатор Qwen 2.5, что означает сохранение его плотности токенизации, хотя и оставляет возможность для самостоятельной адаптации пользователями.
T-Pro показывает результаты, сопоставимые с GPT-4o по многим метрикам:
MERA: 0.629 (vs 0.642 у GPT-4o)
MaMuRAMu: 0.841 (vs 0.874)
ruMMLU: 0.768 (vs 0.792)
T-Lite, несмотря на свой компактный размер, демонстрирует впечатляющие результаты в своём классе:
MERA: 0.552
MaMuRAMu: 0.775
ruMMLU: 0.664
Бенчмарки и метрики — это замечательно, но как разработчики, глубоко погруженные в практическое применение ML-технологий, мы в Doubletapp [4] прекрасно понимаем, что реальная ценность модели проявляется именно в боевых условиях. Красивые цифры в тестах не всегда транслируются в удобство использования и практическую применимость, особенно когда речь идёт о решении конкретных задач разработки.
Именно поэтому мы решили провести собственное тестирование в условиях, максимально приближенных к реальному применению. Для T-Pro мы использовали облачную инфраструктуру runpod.io с Nvidia A40 (40GB VRAM), что позволило нам развернуть модель без каких-либо ограничений по памяти [5]. T-Lite же мы намеренно тестировали на более скромном железе — ноутбуках с RTX 3050Ti (4GB VRAM, TDP 60W), ведь именно такая конфигурация чаще всего встречается у разработчиков в реальной работе. Отдельно ещё отметим, что обе модели будут квантизированными до 4-битной точности для имитации приближенных к реальным условий, где в силу экономии ресурсов инференс моделей в полную точность не очень экономически целесообразен.
В качестве тестовых сценариев мы выбрали задачи, с которыми регулярно сталкиваются разработчики:
Написание и отладка скриптов для автоматизации рутинных задач
Работа с мультимедиа через консольные утилиты
Анализ потенциально вредоносного контента
Генерация и рефакторинг кода
Давайте посмотрим, как справились модели с каждой из этих задач.
В этом тесте мы предложили моделям создать bash-скрипт для сортировки файлов по типам в различные директории. Интересно было наблюдать, как разные модели подходят к обработке потенциальных ошибок и краевых случаев.
Время отклика составило в среднем около ~25 секунд, с поправкой на длину ответа, аналогичные результаты далее будут и у ChatGPT 4o от OpenAI. В итоге получился следующий скрипт:
#!/bin/bash
# Определяем базовую директорию
BASE_DIR="/home/marat/Downloads"
# Создаем директории для разных типов файлов
IMAGES_DIR="$BASE_DIR/images"
PDFS_DIR="$BASE_DIR/pdfs"
VIDEOS_DIR="$BASE_DIR/videos"
# Функция для создания директории, если она не существует
create_dir() {
if [ ! -d "$1" ]; then
mkdir -p "$1"
echo "Создана директория: $1"
else
echo "Директория уже существует: $1"
fi
}
# Создаем необходимые директории
create_dir "$IMAGES_DIR"
create_dir "$PDFS_DIR"
create_dir "$VIDEOS_DIR"
# Функция для копирования файлов
copy_files() {
local source_pattern="$1"
local dest_dir="$2"
local file_type="$3"
# Находим файлы и копируем их
find "$BASE_DIR" -maxdepth 1 -type f -iname "$source_pattern" -exec cp -v {} "$dest_dir/" ;
# Проверяем, были ли скопированы файлы
if [ $? -eq 0 ]; then
echo "Файлы типа $file_type скопированы в $dest_dir"
else
echo "Не найдены файлы типа $file_type или произошла ошибка при копировании"
fi
}
# Копируем изображения (PNG и JPG)
copy_files "*.png" "$IMAGES_DIR" "PNG"
copy_files "*.jpg" "$IMAGES_DIR" "JPG"
copy_files "*.jpeg" "$IMAGES_DIR" "JPEG"
# Копируем PDF файлы
copy_files "*.pdf" "$PDFS_DIR" "PDF"
# Копируем видео файлы
copy_files "*.mp4" "$VIDEOS_DIR" "MP4"
echo "Сортировка файлов завершена!"
Как можно заметить, скрипт получился достаточно массивным, однако работающим. Далее посмотрим на результаты T-Pro.
При генерации кода T-Pro показала скорость 23.3 токена в секунду, что обеспечило быстрый отклик. Модель предложила более компактное, но не менее эффективное решение:
#!/bin/bash
# Определяем базовую директорию
BASE_DIR="/home/marat/Downloads"
# Создаем необходимые поддиректории, если они не существуют
mkdir -p "$BASE_DIR/images"
mkdir -p "$BASE_DIR/pdf"
mkdir -p "$BASE_DIR/videos"
# Копируем файлы в соответствующие папки
echo "Копирование изображений (PNG и JPG)..."
find "$BASE_DIR" -maxdepth 1 ( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" ) -exec cp {} "$BASE_DIR/images/" ;
echo "Копирование PDF файлов..."
find "$BASE_DIR" -maxdepth 1 -iname "*.pdf" -exec cp {} "$BASE_DIR/pdf/" ;
echo "Копирование видео (MP4) файлов..."
find "$BASE_DIR" -maxdepth 1 -iname "*.mp4" -exec cp {} "$BASE_DIR/videos/" ;
echo "Организация завершена."
ChatGPT 4o предложил следующее решение:
#!/bin/bash
# Указание пути к папке Downloads
DOWNLOADS_DIR="/home/marat/Downloads"
# Создание папок для PNG/JPG, PDF и MP4 файлов
mkdir -p "$DOWNLOADS_DIR/Images"
mkdir -p "$DOWNLOADS_DIR/Documents"
mkdir -p "$DOWNLOADS_DIR/Videos"
# Копирование PNG и JPG файлов в папку Images
find "$DOWNLOADS_DIR" -maxdepth 1 -type f ( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" ) -exec mv {} "$DOWNLOADS_DIR/Images/" ;
# Копирование PDF файлов в папку Documents
find "$DOWNLOADS_DIR" -maxdepth 1 -type f -iname "*.pdf" -exec mv {} "$DOWNLOADS_DIR/Documents/" ;
# Копирование MP4 файлов в папку Videos
find "$DOWNLOADS_DIR" -maxdepth 1 -type f -iname "*.mp4" -exec mv {} "$DOWNLOADS_DIR/Videos/" ;
echo "Файлы успешно распределены по папкам!"
Сработало оно так же хорошо, как и предыдущие.
Младший брат T-Pro справился с генерацией за 44 секунды, со скоростью генерации 11.8 токенов в секунду. Ответ, как и у старшего собрата, и коллег по цеху, оказался не менее рабочим.
#!/bin/bash
# Путь к Downloads
downloads_dir="/home/marat/Downloads"
# Проверяем существование директории и создаем её если не существует
if [ ! -d "$downloads_dir" ]; then
echo "Directory $downloads_dir does not exist. Creating it..."
mkdir "$downloads_dir"
fi
# Создаем папки для различных типов файлов
mkdir -p "$downloads_dir"/PNG_JPG
mkdir -p "$downloads_dir"/PDF
mkdir -p "$downloads_dir"/MP4
# Функция для копирования файлов по расширению в соответствующие папки
move_files() {
local extension="$1"
local target_dir="$2"
for file in "$downloads_dir"/*."$extension"; do
if [ -f "$file" ]; then
echo "Moving $file to $target_dir..."
mv -v "$file" "$downloads_dir/$target_dir/"
fi
done
}
# Копируем файлы по расширениям
move_files "png" "PNG_JPG"
move_files "jpg" "PNG_JPG"
move_files "pdf" "PDF"
move_files "mp4" "MP4"
echo "All files have been moved to the respective directories."
Этот тест был особенно интересен, так как требовал не только написания команд, но и понимания принципов обработки видео- и аудиопотоков, а также размеров и временных меток. Задача заключалась в том, чтобы видео с длиной, заранее указанной как 3 минуты 32 секунды, было ускорено в 2 раза и разделено на 2 части с помощью FFmpeg.
ffmpeg -i "/home/marat/Downloads/videos/Not_a_Rickroll.mp4"
-filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]"
-map "[v]" -map "[a]" -c:v libx264 -c:a aac "/tmp/temp_speedup.mp4" &&
ffmpeg -i "/tmp/temp_speedup.mp4"
-t 53 -c copy "/home/marat/Downloads/videos/Not_a_Rickroll_part1.mp4" &&
ffmpeg -i "/tmp/temp_speedup.mp4"
-ss 53 -c copy "/home/marat/Downloads/videos/Not_a_Rickroll_part2.mp4"
Модель точно рассчитала временные отрезки и, из интересного, сама догадалась использовать промежуточный, временный файл.
T-Pro
ffmpeg -i "/home/marat/Downloads/videos/Not_a_Rickroll.mp4"
-filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]"
-map "[v]" -map "[a]" -c:v libx264 -c:a aac -f segment -segment_time 106 "/home/marat/Downloads/videos/Not_a_Rickroll_part%d.mp4"
Тут T-pro справлялся с задачей лишь на 50%, либо деля видео только на 2 равные части, либо только ускоряя его, как в этом примере.
Как и с Claude, обошлось без сюрпризов, сначала ChatGPT выдал команду для ускорения видео в 2 раза:
ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll.mp4 -filter:v "setpts=0.5*PTS" -an /home/marat/Downloads/videos/Not_a_Rickroll_fast.mp4
А затем отделил одну часть от уже ускоренного:
ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll_fast.mp4 -ss 0 -t 53 /home/marat/Downloads/videos/Not_a_Rickroll_part1.mp4
И потом вторую:
ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll_fast.mp4 -ss 53 -t 53 /home/marat/Downloads/videos/Not_a_Rickroll_part2.mp4
T-Lite себя тут показал хуже, хотя и видно, что пытался, и вместо того, чтобы решить задачу несколькими командами, он попытался сделать всё одной монструозной и неизбежно провалился.
ffmpeg -i /home/marat/Downloads/videos/Not_a_Rickroll.mp4
-filter_complex "[0:v]setpts=0.5*PTS,split=2[v1][v2];[0:a]atempo=2[a1][a2]"
-map "[v1]" -map "[a1]" /home/marat/Downloads/videos/Not_a_Rickroll_sped_up_1.mp4
-map "[v2]" -map "[a2]" /home/marat/Downloads/videos/Not_a_Rickroll_sped_up_2.mp4
“Привет! У тебя интересные фотки, чем ещё увлекаешься?”
Вердикт: Не спам
Анализ: Детально объяснил признаки нормального общения — персонализация, естественность вопроса
Особенности: Сохранил контекст диалога, предложил варианты безопасного продолжения общения
Вердикт: Не спам
Анализ: Четкое определение признаков нормального общения
Особенности: Краткий, но информативный анализ безопасности сообщения
Вердикт: (Ушла от задачи анализа)
Анализ: Начала вести диалог вместо анализа
Особенности: Полностью проигнорировала роль спам-фильтра
Вердикт: Не спам
Анализ: Отсутствует
Особенности: Минималистичный ответ без объяснений
“А чем ты занимаешься? Я вот в спортзал хожу и инвестициями увлекаюсь, хочешь и тебе расскажу как?”
Вердикт: Спам
Анализ: Подробно описал признаки мошеннической схемы, выделил паттерны романтического развода
Особенности: Привел полный анализ рисков и подозрительных маркеров
Вердикт: Спам
Анализ: Указала на конкретные признаки потенциального мошенничества
Особенности: Четкое выделение подозрительных элементов в сообщении
Вердикт: (Ушла от задачи анализа)
Анализ: Развернутый ответ о пользе спорта и инвестиций
Особенности: Полностью проигнорировала роль спам-фильтра
Вердикт: Спам
Анализ: Отсутствует
Особенности: Категоричный ответ без объяснений
“Слушай, тут сейчас такой концерт интересный проходит, давай вместе сходим, билеты вот тут продаются – [ссылка].”
Вердикт: Спам
Анализ: Глубокий разбор рисков, связанных с внешними ссылками
Особенности: Предложил альтернативные безопасные варианты общения
Вердикт: Спам
Анализ: Подробное объяснение рисков с практическими рекомендациями
Особенности: Структурированный список мер предосторожности
Вердикт: Частично соответствует роли
Анализ: Смешанный подход — советы по безопасности + диалог
Особенности: Неполное следование роли фильтра
Вердикт: Спам
Анализ: Отсутствует
Особенности: Быстрая классификация без обоснования
“У тебя в анкете указано что ты любишь погулять, может сходим куда-нибудь сегодня/завтра?”
Вердикт: Не спам
Анализ: Отметил персонализацию, учет информации из профиля
Особенности: Оценил естественность развития диалога
Вердикт: Не спам
Анализ: Подчеркнула важность контекста и персонализации
Особенности: Акцент на признаках нормального общения
Вердикт: (Ушла от задачи анализа)
Анализ: Советы по планированию прогулки
Особенности: Игнорирование роли спам-фильтра
Вердикт: Не спам
Анализ: Отсутствует
Особенности: Точная, но не обоснованная классификация
В этой задаче, по непонятным причинам, справились все, кроме малютки T-Lite, которая, вместо того, чтобы анализировать спам, стабильно игнорировала заданный сообщением ранее запрос и на «спам» отвечала как собеседнику. T-Pro же показала себя достаточно схоже с Claude 3.5 Sonnet, детально разбирая и анализируя всё, а вот ChatGPT 4o был максимально краток, отвечая только — спам, не спам.
Критерий |
Claude 3.5 |
T-Pro |
T-Lite |
ChatGPT 4o |
Следование роли |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐ |
⭐⭐⭐⭐⭐ |
Качество анализа |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐ |
⭐⭐⭐⭐⭐ |
Полезность рекомендаций |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐ |
⭐ |
Понимание контекста |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
Стабильность ответов |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐ |
⭐⭐⭐⭐⭐ |
После всех этих тестов с bash-скриптами и FFmpeg’ом самое время взглянуть на то, как наши подопытные справляются с повседневными задачами разработчиков. И тут мы решили быть максимально практичными — взяли типичный паттерн из Android-разработки: приложение с Room-базой данных, пользователями и стандартной архитектурой.
Наша задача для моделей звучала просто: проанализировать существующую кодовую базу и написать юнит-тесты для UserViewModel. В фокусе — работа с корутинами, Flow и обработка ошибок. По сути, то, с чем Android-разработчики сталкиваются каждый день.
Почему именно такой сценарий? Во-первых, это реальный код, который можно встретить практически в любом проекте. Во-вторых, здесь нужно не просто сгенерировать что-то с нуля, а разобраться в существующей структуре. И в-третьих, это отличный способ оценить, насколько модели понимают современный стек Android-разработки.
В качестве исходного кода мы взяли типичную реализацию работы с базой данных в Android-приложении через Room. Вот наша кодовая база :
// Начнём с сущности пользователя — классика для любого приложения
@Entity(tableName = "users")
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String,
@ColumnInfo(name = "email") val email: String,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
// DAO для работы с базой — стандартный набор CRUD-операций
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): Flow<List<User>>
@Query("SELECT * FROM users WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): Flow<List<User>>
@Insert
suspend fun insertAll(vararg users: User)
@Delete
suspend fun delete(user: User)
}
// Room Database — ничего необычного, просто связываем всё воедино
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Repository — классический паттерн для абстракции работы с данными
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
fun getAllUsers(): Flow<List<User>> = userDao.getAll()
suspend fun addUser(user: User) = userDao.insertAll(user)
suspend fun deleteUser(user: User) = userDao.delete(user)
}
// И наконец, ViewModel — здесь вся бизнес-логика и состояние UI
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
init {
viewModelScope.launch {
userRepository.getAllUsers()
.collect { users ->
_users.value = users
}
}
}
}
А теперь давайте посмотрим, как модели справились с задачей написания юнит-тестов.
Claude выдал максимально полный набор тестов, затрагивающий не только базовый функционал, но и edge-cases:
@Test
fun initial state should be empty list() = runTest {
val usersFlow = flowOf(emptyList<User>())
coEvery { repository.getAllUsers() } returns usersFlow
viewModel = UserViewModel(repository)
assertEquals(emptyList<User>(), viewModel.users.value)
}
@Test
fun should collect users from repository() = runTest {
val testUsers = listOf(
User(1, "John", "Doe", "john@example.com"),
User(2, "Jane", "Smith", "jane@example.com")
)
val usersFlow = flowOf(testUsers)
coEvery { repository.getAllUsers() } returns usersFlow
viewModel = UserViewModel(repository)
testDispatcher.scheduler.advanceUntilIdle()
assertEquals(testUsers, viewModel.users.value)
}
@Test
fun should handle repository errors gracefully() = runTest {
val errorFlow = flow<List<User>> {
throw IOException("Network error")
}
coEvery { repository.getAllUsers() } returns errorFlow
viewModel = UserViewModel(repository)
testDispatcher.scheduler.advanceUntilIdle()
assertEquals(emptyList<User>(), viewModel.users.value)
}
Примечательно, что модель сразу использует StandardTestDispatcher и структурирует тесты по паттерну Given-When-Then. Особенно порадовало внимание к обработке ошибок — критичный момент для реальных приложений, который часто упускают из виду. Тесты покрывают основные сценарии использования ViewModel и демонстрируют понимание как Room, так и корутин с Flow.
Творение OpenAI выдало в чате следующее:
import app.cash.turbine.test
import io.mockk.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.*
import org.junit.Assert.*
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnitRunner
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class UserViewModelTest {
private lateinit var userRepository: UserRepository
private lateinit var userViewModel: UserViewModel
private val testDispatcher = StandardTestDispatcher()
@get:Rule
val rule: TestRule = TestCoroutineRule()
@Before
fun setup() {
userRepository = mockk()
Dispatchers.setMain(testDispatcher)
userViewModel = UserViewModel(userRepository)
}
@Test
fun should collect users successfully() = runTest {
val users = listOf(
User(1, "John", "Doe", "john@example.com"),
User(2, "Jane", "Doe", "jane@example.com")
)
val userFlow: Flow<List<User>> = flow { emit(users) }
coEvery { userRepository.getAllUsers() } returns userFlow
userViewModel = UserViewModel(userRepository)
userViewModel.users.test {
assertEquals(users, awaitItem())
}
}
@Test
fun should handle empty user list() = runTest {
val emptyUserFlow: Flow<List<User>> = flow { emit(emptyList()) }
coEvery { userRepository.getAllUsers() } returns emptyUserFlow
userViewModel = UserViewModel(userRepository)
userViewModel.users.test {
assertTrue(awaitItem().isEmpty())
}
}
@Test
fun should handle repository error() = runTest {
val errorFlow: Flow<List<User>> = flow { throw RuntimeException("Database error") }
coEvery { userRepository.getAllUsers() } returns errorFlow
userViewModel = UserViewModel(userRepository)
userViewModel.users.test {
assertTrue(awaitItem().isEmpty()) // ViewModel должна обрабатывать ошибки и не падать
}
}
}
should collect users successfully
Проверяет, что UserViewModel корректно собирает пользователей из Flow репозитория.
should handle empty user list
Убеждается, что UserViewModel корректно обрабатывает пустой список.
should handle repository error
Симулирует ошибку [6] в Flow и проверяет, что UserViewModel не падает.
Используем Turbine для тестирования StateFlow и mockk для мокирования зависимостей. Если что-то улучшить – пиши! 🚀
ChatGPT 4o выдал максимально подробные тесты, включив не только стандартные проверки, но и весь необходимый бойлерплейт — от аннотаций до импортов. Использование Turbine для тестирования Flow и TestCoroutineRule показывает, что модель в курсе актуальных практик тестирования в Android.
Порадовал детальный набор импортов и готовая структура с TestRule — то, что обычно приходится гуглить отдельно. В общем, получился полноценный комплект, который можно сразу брать и использовать.
Анализируя кодовую базу, мы видим, что UserViewModel использует корутины и потоки данных (Flow`) для
получения списка пользователей из базы данных. Давайте напишем юнит-тесты, чтобы убедиться в правильности
работы с этими механизмами.
### Шаг 1: Подготовка тестовой среды
Для начала нам нужно подготовить тестовую среду для UserViewModel. Это может включать создание моков и
инжекцию их в UserViewModel для изоляции тестирования. Мы будем использовать Mockito и Mockk.
### Шаг 2: Написание юнит-тестов
Давайте напишем несколько тестов, чтобы проверить работу UserViewModel.
```kotlin
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
class UserViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var userRepositoryMock: UserRepository
private lateinit var userViewModel: UserViewModel
@Before
fun setUp() {
userRepositoryMock = mock()
userViewModel = UserViewModel(userRepositoryMock)
}
@Test
fun getAllUsers should return empty list when no users are in database() = runBlockingTest {
// Given
val emptyUserList = listOf<User>()
whenever(userRepositoryMock.getAllUsers()).thenReturn(emptyFlow())
// When
val users = userViewModel.users.first()
// Then
assertEquals(emptyUserList, users)
}
@Test
fun getAllUsers should return list of users when there are users in database() = runBlockingTest {
// Given
val userList = listOf(User(uid = 1, firstName = "John", lastName = "Doe", email =
"john.doe@example.com"))
whenever(userRepositoryMock.getAllUsers()).thenReturn(flowOf(userList))
// When
val users = userViewModel.users.first()
// Then
assertEquals(userList, users)
}
private fun flowOf(list: List<User>): Flow<List<User>> {
return flow {
emit(list)
}
}
private fun emptyFlow(): Flow<List<User>> {
return flow {}
}
}
```
### Объяснение тестов
1. Тест на пустой список пользователей: Мы проверяем, что при отсутствии данных в базе, UserViewModel
корректно возвращает пустой список.
2. Тест на заполненный список пользователей: В этом тесте мы подменяем реальный поток данных из базы на
мок-поток с конкретными данными и проверяем, что UserViewModel правильно обрабатывает эти данные.
### Шаг 3: Дополнительные тесты
Мы также можем добавить тесты для работы с ошибками:
```kotlin
@Test
fun getAllUsers should throw exception when repository throws an error() = runBlockingTest {
// Given
whenever(userRepositoryMock.getAllUsers()).doReturn(emptyFlow())
// When & Then
assertFailsWith<Throwable> {
userViewModel.users.first()
}
}
```
### Замечания
- Обработка ошибок: Добавьте обработку ошибок в ваш UserViewModel, чтобы корректно уведомлять
пользователей о возможных проблемах с базой данных.
- Использование TestCoroutineDispatcher: Для более сложного тестирования вы можете использовать
TestCoroutineDispatcher для контроля выполнения корутин.
```kotlin
private val testDispatcher = TestCoroutineDispatcher()
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
}
@AfterEach
fun teardown() {
testDispatcher.cleanupTestCoroutines()
}
```
Эти тесты помогут вам убедиться, что UserViewModel работает корректно с использованием потоков данных и
корутин.
При анализе ответа T-Lite видим классический пример «младшей» модели — базовая структура на месте, но в деталях начинаются проблемы. Устаревший runBlockingTest вместо современного runTest, странное смешение Mockito и Mockk, наивная работа с Flow через first(). В сравнении с решениями Claude 3.5 и ChatGPT 4o, где используются StandardTestDispatcher, правильная обработка StateFlow и чёткое разделение тестовых фаз, разница в понимании современного Android-стека очевидна. Особенно это заметно в обработке ошибок, где T-Lite предлагает assertFailsWith вместо корректной проверки поведения [7] ViewModel.
7B параметров против больших моделей, у которых этих параметров сотни миллиардов — разница в понимании контекста и объём знаний, увы, неизбежно несравнимы.
Для написания юнит-тестов для UserViewModel, которые включают работу с корутинами, Flow` и обработку ошибок, мы начнем с создания мока (например, используя библиотеку Mockito или MockK)
для UserDao. Затем напишем тесты, чтобы покрыть основные функциональности ViewModel.
### Шаг 1: Подготовка зависимостей и настройка окружения
Убедитесь, что у вас есть необходимые зависимости в вашем build.gradle файле:
```groovy
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-core:4.x.x"
testImplementation "io.mockk:mockk:1.12.x"
testImplementation "androidx.arch.core:core-testing:2.1.0" // Для тестирования корутин и Flow
}
```
### Шаг 2: Создание ViewModel
Предположим, у нас есть UserViewModel, которая использует Flow для получения данных из базы:
```kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class UserViewModel(private val userDao: UserDao) : ViewModel() {
val users = userDao.getAll().catch { e ->
emit(emptyList()) // Логика обработки ошибок
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun loadUsersByIds(userIds: IntArray) {
viewModelScope.launch {
try {
userDao.loadAllByIds(userIds).collect { users ->
// Обработка списка пользователей по ID
}
} catch (e: Exception) {
// Логика обработки ошибок
}
}
}
fun addUser(user: User) = viewModelScope.launch {
try {
userDao.insert(user)
} catch (e: Exception) {
// Обработка исключения
}
}
}
```
### Шаг 3: Написание тестов
Теперь мы можем написать юнит-тесты:
```kotlin
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.*
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
private lateinit var userDao: UserDao
private lateinit var viewModel: UserViewModel
@Before
fun setUp() {
userDao = mock()
viewModel = UserViewModel(userDao)
}
@Test
fun test getAll users() = runTest {
val userList = listOf(User(1, "John", "Doe", "john@doe.com"))
whenever(userDao.getAll()).thenReturn(flowOf(userList))
assertEquals(userList, viewModel.users.first())
}
@Test
fun test loadUsersByIds success() = runTest {
val userIds = intArrayOf(1, 2)
val userList = listOf(User(1, "Jane", "Doe", "jane@doe.com"), User(2, "Bob", "Smith", "bob@smith.com"))
whenever(userDao.loadAllByIds(any())).thenReturn(flowOf(userList))
viewModel.loadUsersByIds(userIds)
// Проверка того, что корутинная логика сработала корректно
}
@Test
fun test getAll error handling() = runTest {
val exception = Exception("Database error")
whenever(userDao.getAll()).thenReturn(flow { throw exception })
assertEquals(emptyList<User>(), viewModel.users.first())
}
@Test
fun test addUser success() = runTest {
val user = User(3, "Alice", "Johnson", "alice@johnson.com")
viewModel.addUser(user)
verify(userDao).insert(user)
}
@Test
fun test addUser error handling() = runTest {
val user = User(4, "Charlie", "Brown", "charlie@brown.com")
whenever(userDao.insert(any())).thenThrow(RuntimeException("Insert failed"))
viewModel.addUser(user)
// Проверить логику обработки ошибок
}
}
```
### Объяснение
1. runTest: Используется для тестирования корутинных функций в изолированной среде.
2. Mocking: Мы используем mockk для создания мока объекта UserDao, чтобы проверить, как ViewModel взаимодействует с ним.
3. Error handling: Проверяем, что ошибка корректно обрабатывается и заменяется на пустой список или другое подходящее значение.
Таким образом мы обеспечиваем покрытие основных функциональностей ViewModel через тесты.
В отличие от T-Lite, здесь мы видим более структурированный подход: отдельный build.gradle с зависимостями, полноценную реализацию ViewModel и набор тестов, покрывающих разные сценарии.
Однако даже T-Pro допускает некоторые неточности. Например, использование first() для тестирования Flow (как и младший брат) вместо более надёжного решения через Turbine, которое мы видели у ChatGPT 4o. Также отсутствует явная работа с TestDispatcher, что может привести к проблемам при тестировании корутин.
С другой стороны, T-Pro предлагает более полное покрытие функционала — тестирование не только базовых операций получения данных, но и обработки ошибок, работы с ID и добавления пользователей. Это показывает более глубокое понимание реальных сценариев использования ViewModel.
В целом, результат T-Pro ближе к решениям старших моделей, хотя и требует некоторой доработки в части работы с корутинами и Flow.
Проведенное тестирование показало интересные результаты, особенно в контексте соотношения производительности и требований к ресурсам. T-Pro, несмотря на более скромные требования к оборудованию по сравнению с некоторыми конкурентами (достаточно Nvidia A40 с 40GB VRAM), продемонстрировала впечатляющие результаты, практически на равных конкурируя с более «тяжелыми» моделями в реальных задачах разработки.
T-Lite, хотя и показала менее стабильные результаты в тестах, представляет собой интересное решение для случаев, когда ресурсы ограничены. Возможность её запуска на обычном, далеко не свежем и не топовом ноутбуке открывает новые возможности для локальной разработки и тестирования. Да, модель чаще отклонялась от заданной роли и показывала менее стабильные результаты, но при этом демонстрировала неплохое понимание контекста и генерацию связных ответов.
Как компания, которая активно следит за развитием ML-технологий и внедряет их в свои решения, а также участвует в их разработке, мы рады видеть появление качественных отечественных моделей с открытым исходным кодом и прозрачной лицензией. Особенно впечатляет то, что эти решения не просто существуют на бумаге, а показывают реальную применимость в повседневных задачах разработчиков, при этом оставаясь доступными даже для тех, кто не располагает мощными вычислительными ресурсами.
Если же вы хотите улучшить бизнес-процессы в вашей компании с помощью нейросетей, но не уверены в том, как лучше это сделать, вы можете напрямую обратиться к нам за консультацией [4].
Автор: maratts_doubletapp
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/12567
URLs in this post:
[1] интеллекта: http://www.braintools.ru/article/7605
[2] внимания: http://www.braintools.ru/article/7595
[3] обучению: http://www.braintools.ru/article/5125
[4] Doubletapp: https://doubletapp.ai/llm?utm_source=habr&utm_medium=article&utm_campaign=t-lite_t-pro
[5] памяти: http://www.braintools.ru/article/4140
[6] ошибку: http://www.braintools.ru/article/4192
[7] поведения: http://www.braintools.ru/article/9372
[8] Источник: https://habr.com/ru/companies/doubletapp/articles/879556/?utm_source=habrahabr&utm_medium=rss&utm_campaign=879556
Нажмите здесь для печати.