Тестирование сети и репозиториев
В предыдущей статье мы расширили веб сервер, заставив его возвращать контент локальных файлов. Ниже мы будем расширять тестовый класс и посмотрим как можно работать с репозиториями.
Репозиторий играет ключевую роль как абстрактный источник данных. Это промежуточный слой между бизнес-логикой и представлением. Он предоставляет расширенный функционал по работе с данными (например, проверка закешированных данных перед запросом в сеть).
В парадигме Clean Architecture работа с данными декомпозируется на понятие источник - source
. Например, localSource
предоставляет интерфейс для работы с локальной БД, remoteSource
- для работы с сервером. Основная мысль в том, что имея один интерфейс source
мы можем имплементировать его для работы с разными источниками.
source
и мапперыНапишем общий интерфейс для работы с сетью.
interface RemoteSource {
fun getDisneyCharacters(): List<CartoonCharacter>
}
Его реализацию
class OkRemoteSourceImpl(private val apiTest: ApiTest) : RemoteSource {
override fun getDisneyCharacters(): List<CartoonCharacter> {
val response = apiTest.loadCharacters().execute()
val body = response.body() ?: throw Exception("Failed to load characters")
return body.map { it.transform("Disney") }
}
}
Маппер для преобразования ответа сервера во внутренний объект
fun CharacterResponse.transform(universe: String) = CartoonCharacter(
id = id,
name = name,
universe = universe
)
где сам CartoonCharacter
представлен в виде data
класса
data class CartoonCharacter(
val id: String,
val name: String,
val universe: String
)
Использовать *Response
классы внутри приложения - это крайне плохая идея как минимум потому, что логика на сервере и в приложении может отличаться. Так, в нашем примере мы указали параметр universe
, который позволит нам использовать этот класс для разных источников данных - будь это ApiTest
или ApiMarvel
и проч.
Опишем крайне простой репозиторий для наших целей. Как и в случае с source
опишем общий интерфейс и его реализацию
interface CharacterRepository {
fun getRemoteCharacters(): List<CartoonCharacter>
fun getLocalCharacters(): List<CartoonCharacter>
}
internal class CharacterRepositoryImpl(
private val remoteSource: RemoteSource,
) : CharacterRepository {
override fun getRemoteCharacters(): List<CartoonCharacter> = remoteSource.getDisneyCharacters()
override fun getLocalCharacters(): List<CartoonCharacter> {
TODO("Not yet implemented")
}
}
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class SimpleApiTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var characterRepository: CharacterRepository
@Before
fun setup() {
mockWebServer = createMockServer { path, method ->
when (path) {
"/characters" -> "response_characters"
"/episodes" -> "response_episodes"
else -> null
}
}
val api = createApi<ApiTest>(mockWebServer)
val remoteSource = OkRemoteSourceImpl(api)
characterRepository = CharacterRepositoryImpl(remoteSource)
}
@After
fun shutdown() {
mockWebServer.shutdown()
}
@Test
fun makeApiCall() {
val response = characterRepository.getRemoteCharacters()
val id1 = response.firstOrNull()?.id
Assert.assertEquals("1", id1)
}
}
remoteSource
. С ним аналогично, наружу ему торчать необязательно, нам главное проверять возвращение объектов CartoonCharacter
makeApiCall
делаем проверку на корректную обработку возвращаемого элемента. Помним про особенности List
и несколько раз подумаем точно ли будет возвращаться первый элемент из json
если мы будем делать вызов firstOrNull
к коллекцииГотово! В последней статье про тестирование посмотрим как можно замокать функции в репозитории.