Программное обеспечение не может считаться полноценным без тестирования.

В этой статье я расскажу о том, что такое модульное тестирование, зачем мы тестируем код, как автоматизировать тесты с помощью JUnit и о многом другом.

Что такое модульное тестирование?

Специалисты по программному обеспечению разбивают код на небольшие блоки, чтобы его можно было легко протестировать. Единицами разбиения может служить класс или отдельный метод в классе. Тесты помогают убедиться в том, что код/логика работает так, как задумано.

Зачем тестировать код?

Тесты позволяют удостовериться в том, что поведение приложения соответствует ожиданиям. Если в коде что-то нарушится, мы сразу же узнаем об этом. На ранних этапах разработки можно легко исправить проблему, не отправляя приложение в производство.

Виды тестирования

  • Ручное тестирование. Как следует из названия, приложение тестируется вручную на предмет корректного функционирования. Такой подход отнимает много времени и не очень хорошо масштабируется.
  • Автоматизированное тестирование. В автоматизированных тестах используется фреймворк для тестирования, который охватывает все пограничные случаи для приложения. Такие тесты легко быстро повторить. В контексте Java популярным и хорошо известным фреймворком тестирования считается JUnit. Он же является фреймворком тестирования по умолчанию в Android при создании нового проекта в Android Studio.

В этой статье будем использовать фреймворк тестирования JUnit для написания всех автоматизированных тестов.

Область тестирования

Область тестирования определяет, на какой части приложения сосредоточен тест.

  • Модульные тесты: проверяют малую часть приложения, обычно один класс или один метод в этом классе.
  • Сквозные тесты: проверяют большие части приложения, например весь экран или путь пользователя.
  • Интеграционные тесты: проверяют взаимодействие между несколькими модулями.

Типы тестов в Android определяются тем, где они проводятся.

  1. Локальные модульные тесты выполняются на локальной JVM, поэтому они очень быстрые и обычно включают основную логику, такую как бизнес-логика и математические вычисления. Им не нужно взаимодействовать с фреймворком Android, и они имеют низкий уровень достоверности.
  2. Инструментальные тесты выполняются на физических или эмулированных устройствах Android, поэтому обычно проходят медленно. Как правило, инструментальные тесты представляют собой тесты пользовательского интерфейса (например, тестирование Dao в базе данных Room) и имеют более высокий уровень достоверности.

Уровень достоверности определяет, насколько тест близок к условиям реального производства.За более высокий уровень достоверности приходится платить медленным временем выполнения тестов.Нужно уметь находить баланс между этими показателями.

В этой статье мы сосредоточимся только на написании локальных модульных тестов с помощью JUnit 4.

Среда тестирования

Среда играет решающую роль в контексте тестирования ПО. Без правильно подобранной тестируемой архитектуры нельзя реализовать тестовые случаи надлежащим образом. Android рекомендует многоуровневую архитектуру, о которой можно узнать из этого руководства по архитектуре приложений.

Прежде всего необходимо рассмотреть несколько тестовых случаев, которые обеспечат правильный процесс тестирования.

  1. Изоляция. Изолируйте тестируемый класс от его зависимостей. Используйте для тестирования тестовые дублеры или фиктивные версии зависимых классов.
  2. Синхронизация. Тесты не должны быть асинхронными, в противном случае это приведет к недетерминированному поведению, так называемому flaky test (ненадежному тесту). Если мы работаем с корутинами или классом LiveData, тесты должны быть синхронными.

Ненадежный тест  —  это тест, поведение которого не детерминировано.Иногда он проходит удачно, а иногда  —  нет.

От теории к практике: напишем модульный тест для Viewmodel

Фреймворк тестирования JUnit предоставляет множество аннотаций утверждений для написания тестов (@assertEquals, @assertNotNull, @assertSame и т. д.). Вы можете найти их здесь.

Но в этой статье я буду использовать библиотеку утверждений Truth, которая делает утверждения более читаемыми.

Добавьте следующие зависимости в build.gradle.kts(Module :app) (зависимость для JUnit не нужна, потому что она включена в Android Studio):

testImplementation ("androidx.arch.core:core-testing:2.2.0")
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
testImplementation("androidx.room:room-testing:2.6.1")
testImplementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("androidx.test:core-ktx:1.5.0")
testImplementation("com.google.truth:truth:1.1.5")

Рассмотрим реальный пример тестирования. Как правило, в Android Viewmodel содержит бизнес-логику. В этом примере я буду использовать пример приложения Note.

package com.mubarak.madexample.ui.note

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.mubarak.madexample.R
import com.mubarak.madexample.data.sources.NoteRepository
import com.mubarak.madexample.data.sources.datastore.UserPreference
import com.mubarak.madexample.data.sources.local.model.Note
import com.mubarak.madexample.utils.Event
import com.mubarak.madexample.utils.NoteLayout
import com.mubarak.madexample.utils.NoteStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class HomeNoteViewModel @Inject constructor(
private val noteRepository: NoteRepository,
private val todoPreferenceDataStore: UserPreference
) : ViewModel(), NoteItemAdapter.NoteAdapterListener {


private val _onNoteSwipe = MutableLiveData<Event<Note>>()
val onNoteSwipe: LiveData<Event<Note>> = _onNoteSwipe

private val _onNoteItemClick = MutableLiveData<Event<Note>>()
val onNoteItemClick: LiveData<Event<Note>> = _onNoteItemClick

val getAllNote = noteRepository.getNoteByStatus(NoteStatus.ACTIVE).asLiveData()

private val _noteItemLayout: MutableLiveData<String> = MutableLiveData()
val noteItemLayout: LiveData<String> = _noteItemLayout

// нужно для отображения плейсхолдера в Home Fragment
val isEmpty: LiveData<Boolean> = getAllNote.map { it.isEmpty() }

private val _noteStatusChangeEvent = MutableLiveData<Event<Int>>()
val noteStatusChangeEvent: LiveData<Event<Int>> = _noteStatusChangeEvent


init {
viewModelScope.launch {
_noteItemLayout.value = todoPreferenceDataStore.getNoteLayout.first()
}
}

// переключение между LIST и GRID и наоборот в Home Fragment
fun toggleNoteLayout() {

viewModelScope.launch {
val layout = when (_noteItemLayout.value) {
NoteLayout.LIST.name -> NoteLayout.GRID.name
NoteLayout.GRID.name -> NoteLayout.LIST.name
else -> {
return@launch
}
}

_noteItemLayout.value = layout // обновление измененного значения
todoPreferenceDataStore.setNoteLayout(layout) // сохранение обновленного значения в хранилище данных
}
}

fun redoNoteToActive(noteId: Long) {
viewModelScope.launch {
val note = noteRepository.getNoteById(noteId)
val updateNote = Note(note.id, note.title, note.description, NoteStatus.ACTIVE)
noteRepository.upsertNote(updateNote)
}
}

fun updateNoteStatus(noteId: Long) {
viewModelScope.launch {
val note = noteRepository.getNoteById(noteId)
val updateNote = Note(note.id, note.title, note.description, NoteStatus.ARCHIVE)
noteRepository.upsertNote(updateNote)
_noteStatusChangeEvent.value = Event(R.string.note_archived)
}
}

override fun onNoteItemClicked(note: Note) {
_onNoteItemClick.value = Event(note)
}

override fun onNoteSwipe(note: Note) {
_onNoteSwipe.value = Event(note)
}
}

В классе HomeViewModel нужно протестировать функцию toggleNoteLayout, чтобы проверить, действительно ли она преобразует NoteLayout из List в Grid и наоборот, а также протестировать функцию redoNoteToActive.

Обычно тестирование класса Viewmodel происходит в тестовом исходном наборе, а все, что имеет отношение к визуалу (взаимодействие с представлениями), тестируется в исходном наборе androidTest.

Поместите класс HomeNoteViewModelTest в тестовый исходный набор

Щелкните правой кнопкой мыши на тестовом исходном наборе, далее New > Kotlin Class/File и назовите свой тестовый класс, который вы тестируете, YOUR_CLASS_NAME, затем добавьте Test в конец (в нашем случае  —  HomeNoteViewModelTest).

Шаг 1

Инциализация необходимых классов для HomeNoteViewModel зависит от NoteRepository и UserPreference. И то, и другое  —  интерфейсы. Если вы используете конкретную реализацию, замените ее на интерфейс, чтобы легко заменить реальную реализацию на фиктивную версию для целей тестирования.

Используйте инъекцию зависимостей, когда это возможно. Так вы соблюдете принцип единой ответственности.

/** FakeNoteRepository для тестирования */
class FakeNoteRepository : NoteRepository {

val noteList = mutableListOf<Note>()
override suspend fun insertNote(note: Note) {
noteList.add(note)
}

override suspend fun upsertNote(note: Note) {
val index = noteList.indexOfFirst { it.id == note.id }
if (index != -1) {
noteList[index] = note
} else {
noteList.add(note)
}
}

override fun getAllNote(): Flow<List<Note>> {
return flow { emit(noteList) }
}

override suspend fun deleteNote(note: Note) {
noteList.remove(note)
}

override suspend fun deleteNoteById(noteId: Long) {
noteList.removeAll { it.id == noteId }
}

override suspend fun deleteAllNotes() {
noteList.clear()
}

override suspend fun deleteAllNotesInTrash() {
noteList.clear()
}

override fun searchNote(searchQuery: String): Flow<List<Note>> {
return flow {
val filter = noteList.filter { it.title == searchQuery || it.description == searchQuery }
emit(filter)
}
}

override fun getNoteStreamById(noteId: Long): Flow<Note> {
return flow {noteList.find { it.id == noteId }!! }
}

override fun getNoteByStatus(noteStatus: NoteStatus): Flow<List<Note>> {
return flow {
val filter = noteList.filter { it.noteStatus == noteStatus}
emit(filter)
}
}

override suspend fun getNoteById(noteId: Long): Note {
return noteList.find { it.id == noteId }!!
}
}

Фиктивная версия NoteRepository предназначена только для тестирования. Она не пригодна для использования в производстве.

/** FakeUserPreference для тестирования */
class FakeUserPreference : UserPreference {

private var noteLayout: String = NoteLayout.LIST.name
override suspend fun setNoteLayout(noteLayout: String) {
this.noteLayout = noteLayout
}

override val getNoteLayout: Flow<String>
get() = flow { emit(noteLayout) }
}

Фиктивная версия FakeUserPreference предназначена только для тестирования. Она не может использоваться в производстве.

Теперь у вас есть необходимые зависимости для HomeNoteViewModelTest.kt.

Инициализируем ViewModel с помощью фиктивных зависимостей.

package com.mubarak.madexample.ui.note

import com.mubarak.madexample.data.repository.FakeNoteRepository
import com.mubarak.madexample.data.sources.datastore.FakeUserPreference
import org.junit.Before
import org.junit.Test

class HomeNoteViewModelTest {

private lateinit var homeNoteViewModel: HomeNoteViewModel
private lateinit var fakeNoteRepository: FakeNoteRepository
private lateinit var userPreference: FakeUserPreference

@Before
fun setUp() { // данная функция аннотирована @Before и, следовательно, запускается перед тестом; это лучшее место для инициализации
fakeNoteRepository = FakeNoteRepository()
userPreference = FakeUserPreference()
homeNoteViewModel = HomeNoteViewModel(fakeNoteRepository, userPreference)
}

}

Шаг 2

Напишем тестовый пример для функции toggleNoteLayout.

Создайте тестовую функцию в HomeNoteViewModelTest. В тестовых случаях вам нужно будет следовать определенному соглашению об именовании тестовых функций.

1) Имя функции или класса для тестирования: в данном случае  —  toggleNoteLayout.

2) Действие (Action) или вход (Input), которое/который мы предоставляем функции: в данном случае ListLayout, потому что FakeUserPreference всегда возвращает NoteLayout.LIST.name.

3) Ожидаемый результат, который мы планируем получить от этой тестовой функции. В данном случае toggleNoteLayout должна вернуть противоположный результат, поэтому она возвращает GridLayout.

Таким образом, название функции будет следующим: toggleNoteLayout_ListLayout_ShouldReturnGridLayout. Можно также использовать обратные апострофы (бэктики) для названия функции в локальном тесте, чтобы она выглядела вот так: toggleNoteLayout ListLayout ShouldReturnGridLayout.

Добавьте следующее правило JUnit в HomeNoteViewModelTest: @get:Rule var taskExecutorRule = InstantTaskExecutorRule().

Это правило JUnit применяется для того, чтобы все задания архитектурных компонентов выполнялись в одном потоке для архивации синхронного поведения нашего теста. При написании теста для LiveData обязательно добавляйте это правило.

Протестируем LiveData, чтобы убедиться, что получим в качестве возврата GridLayout при передаче ListLayout. Что касается кэша, то чтобы получить значение LiveData, мы наблюдаем за этим классом. Мы находимся не во Фрагменте или Activity, мы прямо в тестовом классе.

Как наблюдать значение?

Решение заключается в использовании метода observeForever. Вам не нужен LifeCycleOwner. Не забудьте удалить наблюдатель (observer), когда он больше не нужен; если вы забудете это сделать, можете столкнуться с утечкой памяти.

Вот код:

@Test
fun toggleNoteLayout_ListLayout_ShouldReturnGridLayout() {

val homeNoteViewModel = HomeNoteViewModel(fakeNoteRepository,userPreference)

val observer = Observer<String> {}
try {
val actual = homeNoteViewModel.noteItemLayout.value
assertThat(actual).isNotEqualTo(NoteLayout.GRID.name)

} finally {
// удалите наблюдатель
homeNoteViewModel.noteItemLayout.removeObserver(observer)
}
}

Но, похоже, это слишком много для одной тестовой функции. Есть способ решить проблему  —  создать функцию расширения Kotlin для LiveData под названием LiveDataTestObserverUtil:

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(value: T) {
data = value
latch.countDown()
[email protected](this)
}
}
this.observeForever(observer)

try {
afterObserve.invoke()

// Не ждите до бесконечности, если класс LiveData не установлен.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}

} finally {
this.removeObserver(observer)
}

@Suppress("UNCHECKED_CAST")
return data as T
}

Вот как теперь выглядит код:

package com.mubarak.madexample.ui.note

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
import com.mubarak.madexample.MainCoroutineRule
import com.mubarak.madexample.data.repository.FakeNoteRepository
import com.mubarak.madexample.data.sources.datastore.FakeUserPreference
import com.mubarak.madexample.data.sources.local.model.Note
import com.mubarak.madexample.getOrAwaitValue
import com.mubarak.madexample.utils.NoteLayout
import com.mubarak.madexample.utils.NoteStatus
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class HomeNoteViewModelTest {

private lateinit var homeNoteViewModel: HomeNoteViewModel
private lateinit var fakeNoteRepository: FakeNoteRepository
private lateinit var userPreference: FakeUserPreference

@get:Rule
var taskExecutorRule = InstantTaskExecutorRule()

@Before
fun setUp() {
fakeNoteRepository = FakeNoteRepository()
userPreference = FakeUserPreference()
homeNoteViewModel = HomeNoteViewModel(fakeNoteRepository, userPreference)

}

@Test
fun toggleNoteLayout_ListLayout_ShouldReturnGridLayout() {
val actual = homeNoteViewModel.noteItemLayout.getOrAwaitValue()
assertThat(actual).isNotEqualTo(NoteLayout.GRID.name)
}

}

Шаг 3

Утверждаем значения, которые ожидаем получить, и фактическое возвращаемое значение LiveData. Как видите, переменная actual должна возвращать NoteLayout.LIST.name. Когда NoteLayout является List, он должен возвращать Grid, поэтому используем isNotEqualTo.

Запускаем тест, нажав на Run рядом с функцией toggleNoteLayout_ListLayout_ShouldReturnGridLayout.

Тест завершился неудачей и выдал следующее сообщение:

Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used. (Не удалось инициализировать модуль с диспетчером Main. Для тестов можно использовать Dispatchers.setMain из модуля kotlinx-coroutines-test).

Тест провалился

С чем же это связано? Как я уже говорил, при тестировании корутин мы должны выполнять их синхронно. По умолчанию корутины выполняются асинхронно. kotlinx-coroutines предоставляет специальный диспетчер TestDispatcher.

Создайте файл MainCoroutineRule.kt и добавьте в него этот код:

package com.mubarak.madexample

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@ExperimentalCoroutinesApi
class MainCoroutineRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {

override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}

override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}

MainCoroutineRule  —  правило JUnit, которое помогает немедленно выполнить задачу корутины синхронным образом посредством использования этого класса всякий раз, когда мы задействуем область видимости корутины.

Просто добавьте это правило JUnit в тестовый класс:

  • @ExperimentalCoroutinesApi
  • @get:Rule val mainCoroutineRule = MainCoroutineRule() 

В итоге код класса HomeNoteViewModelTest будет выглядеть следующим образом:

package com.mubarak.madexample.ui.note

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
import com.mubarak.madexample.MainCoroutineRule
import com.mubarak.madexample.data.repository.FakeNoteRepository
import com.mubarak.madexample.data.sources.datastore.FakeUserPreference
import com.mubarak.madexample.getOrAwaitValue
import com.mubarak.madexample.utils.NoteLayout
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class HomeNoteViewModelTest {

private lateinit var homeNoteViewModel: HomeNoteViewModel
private lateinit var fakeNoteRepository: FakeNoteRepository
private lateinit var userPreference: FakeUserPreference

@ExperimentalCoroutinesApi
@get:Rule
val mainCoroutineRule = MainCoroutineRule() // не забудьте добавить это правило JUnit, которое мы создаем сейчас

@get:Rule
var taskExecutorRule = InstantTaskExecutorRule()

@Before
fun setUp() {
fakeNoteRepository = FakeNoteRepository()
userPreference = FakeUserPreference()
homeNoteViewModel = HomeNoteViewModel(fakeNoteRepository, userPreference)

}

@Test
fun toggleNoteLayout_ListLayout_ShouldReturnGridLayout() {
val actual = homeNoteViewModel.noteItemLayout.getOrAwaitValue()
assertThat(actual).isNotEqualTo(NoteLayout.GRID.name)
}

}

Тест успешно пройден!

Тест успешно пройден!

Тест, включающий проверку функции приостановки

Напишем еще один тестовый случай для метода redoNoteToActive, который проверяет, правильно ли выполнена конвертация архивной заметки в активную.

Назовите тестовый пример в соответствии с рекомендациями, данными выше. В данном случае тестовый случай будет называться redoNoteToActive_NoteStatus_Archive_ShouldConvertArchiveToActive.

redoNoteToActive  —  это функция приостановки. Как известно, функция приостановки вызывается только в области видимости корутины или другой функции приостановки.

Какую область нам нужно применить для тестирования? Используем runTest  —  он специально создан для тестирования корутин. Помимо прочего, runTest позволяет пропускает задержки.

@Test
fun redoNoteToActive_NoteStatus_Archive_ShouldConvertArchiveToActive() = runTest {
val note = Note(
1,
"Title",
"Description",
NoteStatus.ARCHIVE
)
fakeNoteRepository.insertNote(note)

homeNoteViewModel.redoNoteToActive(noteId = note.id) // Эта функция преобразует архивную заметку в активную
val expected = fakeNoteRepository.getNoteById(note.id)
val actual = Note(
1,
"Title",
"Description",
NoteStatus.ACTIVE
)
assertThat(actual).isEqualTo(expected)
}

Запустите этот тестовый случай. Скорее всего, он пройдет успешно.

Тест пройден успешно!

Выполните те же шаги, что и раньше, чтобы протестировать другие функции в HomeNoteViewModel.

Вот полный список тестовых случаев, которые я добавил в HomeNoteViewModelTest:

package com.mubarak.madexample.ui.note

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
import com.mubarak.madexample.MainCoroutineRule
import com.mubarak.madexample.data.repository.FakeNoteRepository
import com.mubarak.madexample.data.sources.datastore.FakeUserPreference
import com.mubarak.madexample.data.sources.local.model.Note
import com.mubarak.madexample.getOrAwaitValue
import com.mubarak.madexample.utils.NoteLayout
import com.mubarak.madexample.utils.NoteStatus
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class HomeNoteViewModelTest {

private lateinit var homeNoteViewModel: HomeNoteViewModel
private lateinit var fakeNoteRepository: FakeNoteRepository
private lateinit var userPreference: FakeUserPreference

@ExperimentalCoroutinesApi
@get:Rule
val mainCoroutineRule = MainCoroutineRule()

@get:Rule
var taskExecutorRule = InstantTaskExecutorRule()

@Before
fun setUp() {
fakeNoteRepository = FakeNoteRepository()
userPreference = FakeUserPreference()
homeNoteViewModel = HomeNoteViewModel(fakeNoteRepository, userPreference)

}

@Test
fun toggleNoteLayout_ListLayout_ShouldReturnGridLayout() {
val actual = homeNoteViewModel.noteItemLayout.getOrAwaitValue()
assertThat(actual).isNotEqualTo(NoteLayout.GRID.name)
}

@Test
fun redoNoteToActive_NoteStatus_Archive_ShouldConvertArchiveToActive() = runTest {
val note = Note(
1,
"Title",
"Description",
NoteStatus.ARCHIVE
)
fakeNoteRepository.insertNote(note)

homeNoteViewModel.redoNoteToActive(noteId = note.id)
val expected = fakeNoteRepository.getNoteById(note.id)
val actual = Note(
1,
"Title",
"Description",
NoteStatus.ACTIVE
)
assertThat(actual).isEqualTo(expected)
}

@Test
fun redoNoteToActive_NoteStatus_Trash_ShouldConvertTrashToActive() = runTest {
val note = Note(
1,
"Title",
"Description",
NoteStatus.TRASH
)
fakeNoteRepository.insertNote(note)

homeNoteViewModel.redoNoteToActive(noteId = note.id) // преобразует заметку в активную
val expected = fakeNoteRepository.getNoteById(note.id) // теперь она АКТИВНА
val actual = Note(
1,
"Title",
"Description",
NoteStatus.ACTIVE
)
assertThat(actual).isEqualTo(expected)
}

@Test
fun updateNoteStatus_NoteStatus_Active_ShouldConvertActiveToArchive() = runTest{
val note = Note(
1,
"Title",
"Description",
NoteStatus.ACTIVE
)
fakeNoteRepository.insertNote(note)

homeNoteViewModel.updateNoteStatus(noteId = note.id)
val expected = fakeNoteRepository.getNoteById(note.id)
val actual = Note(
1,
"Title",
"Description",
NoteStatus.ARCHIVE
)
assertThat(actual).isEqualTo(expected)
}

@Test
fun getAllNotes_AddSingleNote_ShouldReturnSameNote() = runTest {

fakeNoteRepository.noteList.add(
Note(1,"Note Title","Note Description",NoteStatus.ACTIVE)
)
val noteList = homeNoteViewModel.getAllNote.getOrAwaitValue()
assertThat(noteList).hasSize(1)
assertThat(noteList[0].title).contains("Note Title")
}

}

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Mubarak Native: Unit testing with JUnit in android

Предыдущая статьяОт отправителя к получателю: подход Rust к локальной передаче файлов
Следующая статьяREST API CRUD в Laravel 11 с лучшими практиками