T-Банк выходит на арену: разбираем T-Lite и T-Pro в боевых условиях

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

На российском рынке искусственного интеллекта произошло событие, мимо которого сложно пройти даже самому заядлому скептику — 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-Банка использовала многоступенчатый подход к обучению:

  1. Первичный претрейн на 100B токенов русскоязычных данных из Common Crawl, книг, кода и проприетарных датасетов

  2. Вторичный претрейн на 40B токенов с фокусом на инструктивные данные

  3. SFT (Supervised Fine-Tuning) на 1B токенов для улучшения следования инструкциям

  4. Финальная настройка предпочтений также на 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 должна обрабатывать ошибки и не падать

        }

    }

}

Разбор тестов:

  1. should collect users successfully

    • Проверяет, что UserViewModel корректно собирает пользователей из Flow репозитория.

  2. should handle empty user list

    • Убеждается, что UserViewModel корректно обрабатывает пустой список.

  3. should handle repository error

    • Симулирует ошибку в Flow и проверяет, что UserViewModel не падает.

Используем Turbine для тестирования StateFlow и mockk для мокирования зависимостей. Если что-то улучшить – пиши!

Источник: https://habr.com/ru/companies/doubletapp/articles/879556/


Интересные статьи

Интересные статьи

Если вы зашли прочитать эту статью, есть высокая вероятность, что где-то в вашем списке дел горит ярко-красным "СРОЧНО". Вы, конечно, не горите желанием этим заниматься. Ну что ж, добро пожаловать в к...
Итак после первых плагинов для ВордПресс и джумла пришла пора сделать что-то похожее в Битрикс.Наш путь познания разных движков для сайтов проходит под идеей принести новый функционал, а именно принят...
Евгений Петриченко пришел в Битрикс24 студентом. За 16 лет в компании он прошел путь от специалиста техподдержки до эксперта по мобильной разработке. И ни разу не думал сменить место работы.  ...
В интернете всегда кто-то неправ, и часто встречаются плохие советы по упаковке Docker. Но некоторые из них достаточно опасны, чтобы удостоиться этой статьи. Часто пишут, что при сб...
От скорости сайта зависит многое: количество отказов, брошенных корзин. Согласно исследованию Google, большинство посетителей не ждёт загрузки больше 3 секунд и уходит к конкурентам. Бывает, что сайт ...