Тестирование сети и репозиториев

В предыдущей статье мы расширили веб сервер, заставив его возвращать контент локальных файлов. Ниже мы будем расширять тестовый класс и посмотрим как можно работать с репозиториями.

Теория

Репозиторий играет ключевую роль как абстрактный источник данных. Это промежуточный слой между бизнес-логикой и представлением. Он предоставляет расширенный функционал по работе с данными (например, проверка закешированных данных перед запросом в сеть).

В парадигме 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 внутри приложения

Использовать *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)  
    }  
}
  1. Api интерфейс больше не делаем параметром тестового класса, он закрыт и используется внутри remoteSource. С ним аналогично, наружу ему торчать необязательно, нам главное проверять возвращение объектов CartoonCharacter
  2. В методе makeApiCall делаем проверку на корректную обработку возвращаемого элемента. Помним про особенности List и несколько раз подумаем точно ли будет возвращаться первый элемент из json если мы будем делать вызов firstOrNull к коллекции

Готово! В последней статье про тестирование посмотрим как можно замокать функции в репозитории.