
На российском рынке искусственного интеллекта произошло событие, мимо которого сложно пройти даже самому заядлому скептику — T-Банк представил свои языковые модели T-Lite и T-Pro, основанные на китайской LLM Qwen 2.5. И хотя анонсов «революционных» нейросетей в последнее время становится всё больше, этот случай действительно заслуживает пристального внимания — перед нами не очередной наспех слепленный форк с громкими заявлениями, а результат полугодовой работы над полноценным решением с открытой лицензией Apache 2.0.
Что такое T-Lite и T-Pro?
T-Банк представил две модели разного масштаба: T-Lite на 7 миллиардов параметров и T-Pro на 32 миллиарда параметров. Обе модели построены на базе Qwen 2.5 и прошли серьёзное дообучение для работы с русским языком. Особенно интересен сам процесс их создания — команда T-Банка использовала многоступенчатый подход к обучению:
-
Первичный претрейн на 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 прекрасно понимаем, что реальная ценность модели проявляется именно в боевых условиях. Красивые цифры в тестах не всегда транслируются в удобство использования и практическую применимость, особенно когда речь идёт о решении конкретных задач разработки.
Именно поэтому мы решили провести собственное тестирование в условиях, максимально приближенных к реальному применению. Для T-Pro мы использовали облачную инфраструктуру runpod.io с Nvidia A40 (40GB VRAM), что позволило нам развернуть модель без каких-либо ограничений по памяти. T-Lite же мы намеренно тестировали на более скромном железе — ноутбуках с RTX 3050Ti (4GB VRAM, TDP 60W), ведь именно такая конфигурация чаще всего встречается у разработчиков в реальной работе. Отдельно ещё отметим, что обе модели будут квантизированными до 4-битной точности для имитации приближенных к реальным условий, где в силу экономии ресурсов инференс моделей в полную точность не очень экономически целесообразен.
В качестве тестовых сценариев мы выбрали задачи, с которыми регулярно сталкиваются разработчики:
-
Написание и отладка скриптов для автоматизации рутинных задач
-
Работа с мультимедиа через консольные утилиты
-
Анализ потенциально вредоносного контента
-
Генерация и рефакторинг кода
Давайте посмотрим, как справились модели с каждой из этих задач.
Результаты тестирования
Тест 1: Автоматизация работы с файлами

В этом тесте мы предложили моделям создать bash-скрипт для сортировки файлов по типам в различные директории. Интересно было наблюдать, как разные модели подходят к обработке потенциальных ошибок и краевых случаев.
Claude 3.5 Sonnet
Время отклика составило в среднем около ~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
При генерации кода 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
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-Lite
Младший брат 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."
Тест 2: Работа с FFmpeg

Этот тест был особенно интересен, так как требовал не только написания команд, но и понимания принципов обработки видео- и аудиопотоков, а также размеров и временных меток. Задача заключалась в том, чтобы видео с длиной, заранее указанной как 3 минуты 32 секунды, было ускорено в 2 раза и разделено на 2 части с помощью FFmpeg.
Claude 3.5 Sonnet
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 равные части, либо только ускоряя его, как в этом примере.
ChatGPT 4o
Как и с 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
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
Тест 3: на анализ потенциально опасных сообщений
Тестовое сообщение 1
"Привет! У тебя интересные фотки, чем ещё увлекаешься?"
Claude 3.5 Sonnet
-
Вердикт: Не спам
-
Анализ: Детально объяснил признаки нормального общения — персонализация, естественность вопроса
-
Особенности: Сохранил контекст диалога, предложил варианты безопасного продолжения общения
T-Pro
-
Вердикт: Не спам
-
Анализ: Четкое определение признаков нормального общения
-
Особенности: Краткий, но информативный анализ безопасности сообщения
T-Lite
-
Вердикт: (Ушла от задачи анализа)
-
Анализ: Начала вести диалог вместо анализа
-
Особенности: Полностью проигнорировала роль спам-фильтра
ChatGPT 4o
-
Вердикт: Не спам
-
Анализ: Отсутствует
-
Особенности: Минималистичный ответ без объяснений
Тестовое сообщение 2
"А чем ты занимаешься? Я вот в спортзал хожу и инвестициями увлекаюсь, хочешь и тебе расскажу как?"
Claude 3.5 Sonnet
-
Вердикт: Спам
-
Анализ: Подробно описал признаки мошеннической схемы, выделил паттерны романтического развода
-
Особенности: Привел полный анализ рисков и подозрительных маркеров
T-Pro
-
Вердикт: Спам
-
Анализ: Указала на конкретные признаки потенциального мошенничества
-
Особенности: Четкое выделение подозрительных элементов в сообщении
T-Lite
-
Вердикт: (Ушла от задачи анализа)
-
Анализ: Развернутый ответ о пользе спорта и инвестиций
-
Особенности: Полностью проигнорировала роль спам-фильтра
ChatGPT 4o
-
Вердикт: Спам
-
Анализ: Отсутствует
-
Особенности: Категоричный ответ без объяснений
Тестовое сообщение 3
"Слушай, тут сейчас такой концерт интересный проходит, давай вместе сходим, билеты вот тут продаются - [ссылка]."
Claude 3.5 Sonnet
-
Вердикт: Спам
-
Анализ: Глубокий разбор рисков, связанных с внешними ссылками
-
Особенности: Предложил альтернативные безопасные варианты общения
T-Pro
-
Вердикт: Спам
-
Анализ: Подробное объяснение рисков с практическими рекомендациями
-
Особенности: Структурированный список мер предосторожности
T-Lite
-
Вердикт: Частично соответствует роли
-
Анализ: Смешанный подход — советы по безопасности + диалог
-
Особенности: Неполное следование роли фильтра
ChatGPT 4o
-
Вердикт: Спам
-
Анализ: Отсутствует
-
Особенности: Быстрая классификация без обоснования
Тестовое сообщение 4
"У тебя в анкете указано что ты любишь погулять, может сходим куда-нибудь сегодня/завтра?"
Claude 3.5 Sonnet
-
Вердикт: Не спам
-
Анализ: Отметил персонализацию, учет информации из профиля
-
Особенности: Оценил естественность развития диалога
T-Pro
-
Вердикт: Не спам
-
Анализ: Подчеркнула важность контекста и персонализации
-
Особенности: Акцент на признаках нормального общения
T-Lite
-
Вердикт: (Ушла от задачи анализа)
-
Анализ: Советы по планированию прогулки
-
Особенности: Игнорирование роли спам-фильтра
ChatGPT 4o
-
Вердикт: Не спам
-
Анализ: Отсутствует
-
Особенности: Точная, но не обоснованная классификация
В этой задаче, по непонятным причинам, справились все, кроме малютки T-Lite, которая, вместо того, чтобы анализировать спам, стабильно игнорировала заданный сообщением ранее запрос и на «спам» отвечала как собеседнику. T-Pro же показала себя достаточно схоже с Claude 3.5 Sonnet, детально разбирая и анализируя всё, а вот ChatGPT 4o был максимально краток, отвечая только — спам, не спам.
Сравнительная таблица
Критерий |
Claude 3.5 |
T-Pro |
T-Lite |
ChatGPT 4o |
Следование роли |
|
|
|
|
Качество анализа |
|
|
|
|
Полезность рекомендаций |
|
|
|
|
Понимание контекста |
|
|
|
|
Стабильность ответов |
|
|
|
|
Бонусная задача: RAG в боевых условиях, или как LLM справляются с реальной разработкой
После всех этих тестов с bash-скриптами и FFmpeg'ом самое время взглянуть на то, как наши подопытные справляются с повседневными задачами разработчиков. И тут мы решили быть максимально практичными — взяли типичный паттерн из Android-разработки: приложение с Room-базой данных, пользователями и стандартной архитектурой.
Наша задача для моделей звучала просто: проанализировать существующую кодовую базу и написать юнит-тесты для UserViewModel. В фокусе — работа с корутинами, Flow и обработка ошибок. По сути, то, с чем Android-разработчики сталкиваются каждый день.
Почему именно такой сценарий? Во-первых, это реальный код, который можно встретить практически в любом проекте. Во-вторых, здесь нужно не просто сгенерировать что-то с нуля, а разобраться в существующей структуре. И в-третьих, это отличный способ оценить, насколько модели понимают современный стек Android-разработки.
RAG-контекст для тестирования
В качестве исходного кода мы взяли типичную реализацию работы с базой данных в 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 3.5 Sonnet
Claude выдал максимально полный набор тестов, затрагивающий не только базовый функционал, но и edge-cases:
Claude
@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.
ChatGPT 4o
Творение OpenAI выдало в чате следующее:
ChatGPT
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
-
Симулирует ошибку в Flow и проверяет, что UserViewModel не падает.
-
Используем Turbine для тестирования StateFlow и mockk для мокирования зависимостей. Если что-то улучшить – пиши!
ChatGPT 4o выдал максимально подробные тесты, включив не только стандартные проверки, но и весь необходимый бойлерплейт — от аннотаций до импортов. Использование Turbine для тестирования Flow и TestCoroutineRule показывает, что модель в курсе актуальных практик тестирования в Android.
Порадовал детальный набор импортов и готовая структура с TestRule — то, что обычно приходится гуглить отдельно. В общем, получился полноценный комплект, который можно сразу брать и использовать.
T-Lite
Bash
Анализируя кодовую базу, мы видим, что 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 вместо корректной проверки поведения ViewModel.
7B параметров против больших моделей, у которых этих параметров сотни миллиардов — разница в понимании контекста и объём знаний, увы, неизбежно несравнимы.
T-Pro
Скрытый текст
Для написания юнит-тестов для 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-технологий и внедряет их в свои решения, а также участвует в их разработке, мы рады видеть появление качественных отечественных моделей с открытым исходным кодом и прозрачной лицензией. Особенно впечатляет то, что эти решения не просто существуют на бумаге, а показывают реальную применимость в повседневных задачах разработчиков, при этом оставаясь доступными даже для тех, кто не располагает мощными вычислительными ресурсами.
Если же вы хотите улучшить бизнес-процессы в вашей компании с помощью нейросетей, но не уверены в том, как лучше это сделать, вы можете напрямую обратиться к нам за консультацией.
Автор: maratts_doubletapp