Generalidades

Nuestro recorrido por la aplicación de Android comprobable | Autor: Malvern Sutanto | Proyecto de reclutamiento | Junio ​​de 2021

Marvin Sutanto

¿Cómo refactorizamos nuestra antigua base de código de Android para hacerla comprobable?

Foto de Patrick Fore en Unsplash

La aplicación Wantedly para Android se lanzó por primera vez en 2014, hace un tiempo, por lo que nuestra base de código de la aplicación para Android es bastante antigua. Cuando me uní a Wantedly hace 3 años, había muchos problemas con el conjunto de pruebas existente. El número de casos de prueba es pequeño y no se puede generar el informe de cobertura de prueba. Los casos de prueba que tenemos también son muy inestables y difíciles de mantener. Siempre que queremos actualizar o agregar nuevos casos de prueba, necesitamos realizar muchas configuraciones complicadas y es difícil probar los componentes individuales de la aplicación de forma independiente.

Sabemos que si queremos expandir nuestro desarrollo y aumentar la productividad de los ingenieros de Android, necesitamos mejorar esto. En este artículo, quiero compartir nuestro trabajo para hacer que nuestra base de código de Android sea comprobable.

Creo que es muy importante definir qué debemos implementar y lograr. En nuestro caso, creamos un documento que describe los objetivos, beneficios, estrategias y convenciones de las pruebas de redacción. El documento también se utiliza como contenido central de la discusión entre ingenieros y partes interesadas, de modo que todos están en la misma página y también se puede utilizar como referencia para nuevos miembros en el futuro. Intentaré mostrar brevemente cómo es nuestro documento.

la meta: Cree un marco e infraestructura sólidos para crear, probar y publicar nuestras aplicaciones fácilmente.

beneficio: Los conjuntos de pruebas confiables permiten a los ingenieros realizar cambios rápidamente y lanzar aplicaciones con confianza. Proporciona un ciclo de retroalimentación más corto durante el proceso de desarrollo, lo que facilita la depuración de problemas en la aplicación. También nos permite simular de manera consistente casos extremos que son difíciles de replicar. También se puede utilizar un conjunto de pruebas bien documentado como documento para ayudar a compartir conocimientos.

estrategia: Utilice la pirámide de prueba para construir un conjunto de pruebas (discutiremos esto en detalle más adelante). También necesitamos que cada «componente» de la aplicación se pueda probar de forma independiente, pero aún así se puedan probar juntos si es necesario. Ejecutaremos el conjunto de pruebas en cada solicitud de extracción. Cualquier solicitud de extracción a la sucursal principal debe agregar / eliminar casos de prueba, y también nos aseguraremos de que la tasa de cobertura no disminuya.

Convención: Asegúrese de que los casos de prueba sean coherentes en toda la base del código.Usaremos un método de prueba uniforme para nombrar (similar a givenCondition_whenAction_thenResult) Para garantizar la coherencia y facilitar la redacción y el mantenimiento. La nomenclatura consistente del método de prueba nos permite identificar rápidamente qué caso de prueba falló en CI. El caso de prueba debe ser pequeño y solo probar un aspecto del código.

Pirámide de prueba

La pirámide de pruebas nos permite agrupar las pruebas en depósitos según la cantidad de componentes involucrados, el tiempo de ejecución y el trabajo de mantenimiento de cada grupo. También nos da una idea de cuántos casos de prueba deben escribirse para cada grupo. Idealmente, desea escribir una gran cantidad de casos de prueba que sean fáciles de mantener. Por lo tanto, cuando subes a la pirámide, necesitas un número menor de pruebas. Por lo general, comenzando desde la parte superior, realizará pruebas de extremo a extremo, luego pruebas de integración y finalmente pruebas unitarias en la parte inferior de la pirámide.

Así es como usamos la pirámide de prueba para construir un conjunto de pruebas:

En la parte superior de la pirámide, usamos Firebase Robo Test para realizar pruebas de un extremo a otro.Entonces tenemos nuestro Fragments‘Prueba de interfaz de usuario de Espresso, ejecutada como prueba de instrumento.Probamos Fragments versus ViewModel Como el nuestro Fragments Estrechamente relacionado con nuestra implementación ViewModel. También reemplazamos ViewModel (Como la clase de repositorio) y la prueba se duplica para permitir una prueba más fácil. Finalmente, tenemos el repositorio y las pruebas de la clase de utilidad que usan Robolectric como ejecución de prueba de unidad local.

Nota: Las clases de repositorio que interactúan con bases de datos SQLite deben ejecutarse como pruebas de instrumentación.

Prueba de dobles

caries en los dientes: Un doble de prueba que puede responder a un conjunto de llamadas a métodos con respuestas definidas. También puede comprobar si el método en la prueba doble se ha llamado o no se ha llamado todavía. Algunas bibliotecas populares que pueden ayudarlo a crear clases simuladas en Android son Mockito y MockK.

interface UserRepository 
suspend fun getUser(userId: Int): User
// Create a mock UserRepository with MockK
val mockRepo = mockk<UserRepository>()
// Set getUser to return a fake User object every time it's called.
coEvery mockRepo.getUser(any()) answers
User(
id = firstArg(),
firstName = "John",
lastName = "Smith"
)
// Usage
launch
mockRepo.getUser(1) // Will return User(1, "John", "Smith")

Falsificado: Un doble de prueba que usa algún tipo de implementación de «atajo» para comportarse como una clase real. Si usa interfaces para declarar las dependencias de su clase, crear una implementación falsa es muy simple.

interface UserRepository 
suspend fun getUser(userId: Int): User
// Create a fake implementation of a UserRepository
class FakeUserRepository : UserRepository
override suspend fun getUser(userId: Int): User
return User(
id = userId,
firstName = "John",
lastName = "Smith"
)

Prueba de escritura

Además, también usamos otras bibliotecas de terceros para ayudarnos a escribir casos de prueba más fácilmente. Por ejemplo, usamos Kakao (un contenedor Kotlin DSL para Espresso) para escribir nuestras pruebas de IU porque encontramos que el DSL es más legible. También escribimos una biblioteca para usar la reflexión para crear objetos falsos que ayuden a generar datos falsos para realizar pruebas.

Dado que la mayoría de los componentes que estamos probando son similares entre sí (Fragments, ViewModels, y muchos más). Creamos algunas reglas JUnit para simplificar la configuración de estas pruebas.Por ejemplo, tenemos un FragmentTestRule Contiene varias otras reglas JUnit, como InstantTaskExecutorRule, TaskSchedulerRule, con DataBindingIdlingResourceRule Para asegurar que el Fragment La prueba es determinista, no escamosa.

Nuestro enfoque es probar la interfaz de usuario para probar la lógica de la interfaz de usuario, no la apariencia de los elementos de la interfaz de usuario.Compruebe si uno Button Usar Espresso para mostrar correctamente en un color o tamaño específico es difícil, pero puede verificar fácilmente si el botón tiene la etiqueta correcta o si se puede hacer clic.Si quieres probar una apariencia ButtonTe sugiero que eches un vistazo a la prueba de captura de pantalla.

Aislar los componentes de la prueba

interface UserApi 
suspend fun getUser(userId: Int): User
interface UserRepository
suspend fun getUser(userId: Int): Flow<User>
class UserRepositoryImpl(
val userApi: UserApi,
val userDb: UserDB
) : UserRepository

override fun getUser(userId: Int): Flow<User>
// Fetches user data from UserAPI and store it
// in the local DB if necessary.
...

class UserViewModel(
private val userId: Int,
private val userRepository: UserRepository
) : ViewModel()
private val _user: MutableLiveData<User> = MutableLiveData()
val user: LiveData<User> = _user

init
viewModelScope.launch
// Observe changes from UserRepository.
userRepository.getUser(userId).collect user ->
_user.value = user



class UserFragment : Fragment()

// Assume that UserViewModel is injected through Dagger.
@Inject
lateinit var viewModel: UserViewModel

override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
...

// Observe state changes from LiveData.
viewModel.user.observe ...

someButton.setOnClickListener
// Use Navigation Component to handle navigation events.
findNavController().navigate(R.id.to_other_destination)


Al probar UserRepository, Podemos reemplazar la ejecución UserApi como esto:

@RunWith(AndroidJUnit4::class)
class UserRepositoryTest
@JvmField
@Rule
val repositoryTestRule = RepositoryTestRule()

@MockK
lateinit var userApi: UserApi

@Before
fun setUp()
MockKAnnotations.init(this)

@Test
fun givenUserApiReturnsUser_ThenEmitUser() = runBlockingTest

// Set up mocked UserApi to return a fake user.
coEvery userApi.getUser(any()) answers
User(
id = firstArg(),
firstName = "John",
lastName = "Smith"
)

// Replace the dependencies of UserRepository with mocks.
val userRepository = UserRepositoryImpl(
userApi,
repositoryTestRule.db
)

// Assert that repository emits the fake user.
val user = userRepository.getUser().single()
assertEquals("John", user.firstName)
assertEquals("Smith", user.lastName)

...

Al probar UserViewModel, Solo necesitamos reemplazar UserRepository llevado a cabo:

@RunWith(AndroidJUnit4::class)
class UserViewModelTest
@JvmField
@Rule
val viewModelTestRule = ViewModelTestRule()

@MockK
lateinit var userRepository: UserRepository

@Before
fun setUp()
MockKAnnotations.init(this)

@Test
fun givenUserRepostiroyEmitsUser_ThenUserLiveDataIsUpdated()

// Set user repository to emit a fake user.
coEvery userRepository.getUser(any()) answers
flowOf(
User(
id = firstArg(),
firstName = "John",
lastName = "Smith"
)
)

// Replace the dependencies of UserViewModel with mocks.
val viewModel = UserViewModel(1, userRepository)

// Assert that user LiveData emits the fake user.
val user = viewModel.user.getOrAwaitValue()
assertEquals("John", user.firstName)
assertEquals("Smith", user.lastName)

Puedes encontrar getOrAwaitValue La extensión está aquí.

Como mencioné antes, generalmente probamos nuestro Fragment con ViewModel Juntos, así es como probamos UserFragment:

@RunWith(AndroidJUnit4::class)
class UserFragmentTest
@JvmField
@Rule
val fragmentTestRule = FragmentTestRule()

@MockK
lateinit var navController: NavController

@MockK
lateinit var userRepository: UserRepository

@Before
fun setUp()
MockKAnnotations.init(this)

// Launch the fragment using FragmentScenario.
private fun launchFragment(): FragmentScenario<UserFragment>
return launchFragmentInContainer

// Create the Fragment manually and replace any
// dependencies as needed.
UserFragment().also userFragment ->
userFragment.viewModel = UserViewModel(1, userRepository)

fragmentTestRule.dataBindingIdlingResourceRule
.setFragment(userFragment)

.onFragment userFragment ->

// Replace the NavController with a mocked NavController
// to test UserFragment in isolation.
Navigation.setViewNavController(
it.requireView(),
navController
)


@Test
fun givenUser_ThenDisplayUserDetails()

// Set fake user similar to ViewModel.
coEvery userRepository.getUser(any()) answers
flowOf(
User(
id = firstArg(),
firstName = "John",
lastName = "Smith"
)
)

launchFragment()

// Assert views with Kakao.
onScreen<UserScreen>
firstNameTextView.hasText("John")
lastNameTextView.hasText("Smith")

@Test
fun whenClickSomeButton_ThenNavigateToOtherDetailsFragment()
launchFragment()

// Trigger UI interactions with Kakao.
onScreen<UserScreen>
someButton.click()

// Verify navigation events using the mocked NavController.
verify navController.navigate(R.id.to_other_destination)

También puedes usar TestNavHostController Como tu sustituto NavController.

Esta configuración también nos permite combinar componentes fácilmente durante las pruebas de integración.Por ejemplo, podemos usar Dagger para reemplazar solo UserApi Utilice un cliente de API simulado durante las pruebas y mantenga las dependencias restantes coherentes con la implementación real.

Solicitudes de extracción y cobertura

Dado que estamos tratando con código heredado, debemos introducir gradualmente conjuntos de pruebas. Es imposible refactorizar toda la aplicación a la vez, porque con este método no podremos desarrollar ninguna característica en paralelo. Si no hay un desarrollo paralelo, no podremos proporcionar a los usuarios actualizaciones o correcciones. Por lo tanto, decidimos escribir casos de prueba para los componentes en los que estamos trabajando o refactorizando actualmente, y usar la cobertura de código para rastrear nuestro progreso general.

El informe de cobertura de código nos permite averiguar fácilmente qué componentes se han probado o no. Además, también introdujimos un umbral de cobertura mínimo y lo aumentamos gradualmente para garantizar que no agreguemos más código no probado a la base del código.

  • La implementación real no coincide con el documento de prueba. Cuando creamos nuestro documento de prueba por primera vez, había muchas incógnitas. Lo que finalmente logramos es completamente diferente de lo que queríamos originalmente. Por lo tanto, debemos probar el documento de forma iterativa. Necesitamos adaptarnos a los cambios tecnológicos y ajustar nuestros métodos en consecuencia. Por ejemplo, queremos usar la sintaxis de pepinillo en Spek para ayudarnos a construir suites de prueba. Sin embargo, Spek no admite actualmente las pruebas de instrumentación para Android. Dado que queríamos mantener la coherencia entre las pruebas de unidades locales y las pruebas de instrumentos (como queríamos el proyecto Nitrogen), decidimos no utilizar Spek.
  • Se necesita mucho tiempo para escribir y mantener conjuntos de pruebas automatizados. La implementación de los casos de prueba correctos requerirá inevitablemente más tiempo. Nosotros, como ingenieros, necesitamos comunicarnos con las partes interesadas y gestionar sus expectativas. Sin embargo, creo que escribir pruebas automatizadas es una buena inversión a largo plazo, porque la mayoría de las pruebas manuales son muy repetitivas y pueden automatizarse. Invertir en suites de prueba automatizadas también nos permite cambiar el código más rápido.
  • A medida que crece el conjunto de pruebas, el tiempo de ejecución aumentará. El número y los tipos de pruebas en el conjunto de pruebas afectarán seriamente el tiempo de ejecución de la canalización de pruebas. Si hay muchas pruebas de IU, el tiempo de ejecución puede ser muy largo y ralentizará la velocidad de desarrollo. Para nosotros, tenemos alrededor de 1000 pruebas unitarias y 300 UI / pruebas de integración. Se necesitan aproximadamente 5 minutos de pruebas unitarias y 15 minutos de pruebas de IU para ejecutar nuestro conjunto de pruebas en nuestro servidor CI. Incluso después de que presentamos Flank para dividir nuestras pruebas, lleva mucho tiempo realizar pruebas de IU. Si tiene un conjunto de pruebas más grande, es posible que desee consultar el método de Dropbox, que ejecuta de forma selectiva el conjunto de pruebas para detectar el código que ha cambiado o que puede verse afectado por el cambio.
  • Usar Jacoco para generar cobertura de código puede ser difícil. Aunque el complemento de Android Gradle es compatible con Jacoco de fábrica. Puede ser muy difícil de configurar, especialmente al actualizar la versión de AGP. Según nuestra experiencia, cuando se actualiza AGP, Jacoco a menudo arroja errores de compilación o muestra informes de cobertura incorrectos. Como puede ver en nuestro gráfico de cobertura de sucursales, cuando actualizamos AGP a 4.2, nuestra cobertura se redujo significativamente debido a este problema. Hay otros temas que necesitan atención, como este, este y este.

Los conjuntos de pruebas automatizados pueden ayudarlo a identificar rápidamente las regresiones durante el proceso de desarrollo. Sin embargo, no creo que pueda confiar en las suites de prueba automatizadas al 100%, porque los procesos de control de calidad manuales pueden encontrar más fácilmente problemas como problemas de animación, problemas de interfaz de usuario o de diseño, uso incorrecto de dobles de prueba o casos extremos no manejados. Por lo tanto, sigue siendo importante realizar pruebas manuales durante su ciclo de desarrollo.

LEER  Sube imágenes y archivos directamente desde el teléfono al servidor - ReactNative | Autor: Manoj Kanth | Enero de 2022

Publicaciones relacionadas

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Botón volver arriba