Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Как я перестал работать по выходным и полюбил тесты

Как я перестал работать по выходным и полюбил тесты

Сергей Зароченцев, RedMadRobot at #MOSDROID 17 Chlorine

Как перестать бояться и начать писать тесты? Как получить выгоду от написания тестов? Поделюсь своим практическим опытом, приемами и техниками тестирования в жестком мире аутсорс-разработки. Поговорим о том, как сфокусироваться на самом важном и написать действительно хорошие тесты

MOSDROID

May 30, 2019
Tweet

More Decks by MOSDROID

Other Decks in Programming

Transcript

  1. Тесты это… 01 Отличная документация на код 02 Тестируемость -

    лакмусовая бумажка хорошей архитектуры 03 Сразу думаем о краевых и негативных сценариях 04 Нет боязни менять чужой код Redmadrobot Тестирование 05 Экономия времени
  2. Тестирование Что мы хотим от тестов? Redmadrobot 01 02 03

    Просто писать, большое покрытие Изменения без страха Сложная логика, которую тяжело проверить руками
  3. Самый лучший тест - ручной. Проверяется весь функционал целиком. Но

    очень дорого и долго Просто писать. Большое покрытие Самый лучший тест Redmadrobot
  4. UI тесты тоже очень дорогие. Их трудно писать. Их тяжело

    запускать, они падают и работают долго Просто писать. Большое покрытие UI тесты Redmadrobot
  5. Просто писать. Большое покрытие Опишем State экрана data class CardAdditionViewState(

    val cardNumberState: CardNumberState, val isAddCardButtonEnabled: Boolean, val screenState: ScreenState) { data class CardNumberState(val number: String, val error: CardNumberError) { enum class CardNumberError { NOT_MIR_PAYMENT_SYSTEM_CARD, LUHN_CHECK_INCORRECT, NONE } } sealed class ScreenState { object Content : ScreenState() object Loading : ScreenState() data class Error(val error: Throwable) : ScreenState() object Success : ScreenState() } }
  6. Просто писать. Большое покрытие Опишем State экрана data class CardAdditionViewState(

    val cardNumberState: CardNumberState, val isAddCardButtonEnabled: Boolean, val screenState: ScreenState) { data class CardNumberState(val number: String, val error: CardNumberError) { enum class CardNumberError { NOT_MIR_PAYMENT_SYSTEM_CARD, LUHN_CHECK_INCORRECT, NONE } } sealed class ScreenState { object Content : ScreenState() object Loading : ScreenState() data class Error(val error: Throwable) : ScreenState() object Success : ScreenState() } }
  7. Просто писать. Большое покрытие Опишем State экрана data class CardAdditionViewState(

    val cardNumberState: CardNumberState, val isAddCardButtonEnabled: Boolean, val screenState: ScreenState) { data class CardNumberState(val number: String, val error: CardNumberError) { enum class CardNumberError { NOT_MIR_PAYMENT_SYSTEM_CARD, LUHN_CHECK_INCORRECT, NONE } } sealed class ScreenState { object Content : ScreenState() object Loading : ScreenState() data class Error(val error: Throwable) : ScreenState() object Success : ScreenState() } }
  8. Просто писать. Большое покрытие Опишем State экрана data class CardAdditionViewState(

    val cardNumberState: CardNumberState, val isAddCardButtonEnabled: Boolean, val screenState: ScreenState) { data class CardNumberState(val number: String, val error: CardNumberError) { enum class CardNumberError { NOT_MIR_PAYMENT_SYSTEM_CARD, LUHN_CHECK_INCORRECT, NONE } } sealed class ScreenState { object Content : ScreenState() object Loading : ScreenState() data class Error(val error: Throwable) : ScreenState() object Success : ScreenState() } }
  9. Просто писать. Большое покрытие ViewModel и State class CardAdditionViewModel :

    BaseViewModel() { val state = MutableLiveData<CardAdditionViewState>() private var viewState: CardAdditionViewState = CardAdditionViewState.default set(value) { field = value state.onNext(field) } } override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel = obtainViewModel(CardAdditionViewModel.Factory(screenOpenContext)) observe(viewModel.state, this::render) } private fun render(viewState: CardAdditionViewState) { renderCardNumberViewState(viewState.cardNumberState) additionButton.isEnabled = viewState.isAddCardButtonEnabled renderScreenState(viewState.screenState) }
  10. Просто писать. Большое покрытие ViewModel и State class CardAdditionViewModel :

    BaseViewModel() { val state = MutableLiveData<CardAdditionViewState>() private var viewState: CardAdditionViewState = CardAdditionViewState.default set(value) { field = value state.onNext(field) } } override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel = obtainViewModel(CardAdditionViewModel.Factory(screenOpenContext)) observe(viewModel.state, this::render) } private fun render(viewState: CardAdditionViewState) { renderCardNumberViewState(viewState.cardNumberState) additionButton.isEnabled = viewState.isAddCardButtonEnabled renderScreenState(viewState.screenState) }
  11. Просто писать. Большое покрытие ViewModel и State class CardAdditionViewModel :

    BaseViewModel() { val state = MutableLiveData<CardAdditionViewState>() private var viewState: CardAdditionViewState = CardAdditionViewState.default set(value) { field = value state.onNext(field) } } override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel = obtainViewModel(CardAdditionViewModel.Factory(screenOpenContext)) observe(viewModel.state, this::render) } private fun render(viewState: CardAdditionViewState) { renderCardNumberViewState(viewState.cardNumberState) additionButton.isEnabled = viewState.isAddCardButtonEnabled renderScreenState(viewState.screenState) }
  12. Просто писать. Большое покрытие ViewModel и State class CardAdditionViewModel :

    BaseViewModel() { val state = MutableLiveData<CardAdditionViewState>() private var viewState: CardAdditionViewState = CardAdditionViewState.default set(value) { field = value state.onNext(field) } } override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel = obtainViewModel(CardAdditionViewModel.Factory(screenOpenContext)) observe(viewModel.state, this::render) } private fun render(viewState: CardAdditionViewState) { renderCardNumberViewState(viewState.cardNumberState) additionButton.isEnabled = viewState.isAddCardButtonEnabled renderScreenState(viewState.screenState) }
  13. Просто писать. Большое покрытие ViewModel и State class CardAdditionViewModel :

    BaseViewModel() { val state = MutableLiveData<CardAdditionViewState>() private var viewState: CardAdditionViewState = CardAdditionViewState.default set(value) { field = value state.onNext(field) } } override fun onActivityCreated(savedInstanceState: Bundle?) { viewModel = obtainViewModel(CardAdditionViewModel.Factory(screenOpenContext)) observe(viewModel.state, this::render) } private fun render(viewState: CardAdditionViewState) { renderCardNumberViewState(viewState.cardNumberState) additionButton.isEnabled = viewState.isAddCardButtonEnabled renderScreenState(viewState.screenState) }
  14. Просто писать. Большое покрытие TDD @Test fun `when number starts

    with 2 - then error should not be show`() { // Given val viewModel = CardAdditionViewModel( schedulersProvider = TrampolineSchedulersProvider(), context = mock(), router = mock(), bindCardUseCase = mock(), ) val mirCardNumber = "2345" // When viewModel.onCardNumberEntered(mirCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NONE) }
  15. Просто писать. Большое покрытие Название @Test fun `when number starts

    with 2 - then error should not be show`() { // Given val viewModel = CardAdditionViewModel( schedulersProvider = TrampolineSchedulersProvider(), context = mock(), router = mock(), bindCardUseCase = mock(), ) val mirCardNumber = "2345" // When viewModel.onCardNumberEntered(mirCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NONE) }
  16. Просто писать. Большое покрытие Given When Then @Test fun `when

    number starts with 2 - then error should not be show`() { // Given val viewModel = CardAdditionViewModel( schedulersProvider = TrampolineSchedulersProvider(), context = mock(), router = mock(), bindCardUseCase = mock(), ) val mirCardNumber = "2345" // When viewModel.onCardNumberEntered(mirCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NONE) }
  17. Просто писать. Большое покрытие Тестирование RxJava @Test fun `when number

    starts with 2 - then error should not be show`() { // Given val viewModel = CardAdditionViewModel( schedulersProvider = TrampolineSchedulersProvider(), context = mock(), router = mock(), bindCardUseCase = mock(), ) val mirCardNumber = "2345" // When viewModel.onCardNumberEntered(mirCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NONE) }
  18. Просто писать. Большое покрытие Mocks and Stubs @Test fun `when

    number starts with 2 - then error should not be show`() { // Given val viewModel = CardAdditionViewModel( schedulersProvider = TrampolineSchedulersProvider(), context = mock(), router = mock(), bindCardUseCase = mock(), ) val mirCardNumber = "2345" // When viewModel.onCardNumberEntered(mirCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NONE) }
  19. Просто писать. Большое покрытие AssertJ @Test fun `when number starts

    with 2 - then error should not be show`() { // Given val viewModel = CardAdditionViewModel( schedulersProvider = TrampolineSchedulersProvider(), context = mock(), router = mock(), bindCardUseCase = mock(), ) val mirCardNumber = "2345" // When viewModel.onCardNumberEntered(mirCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NONE) }
  20. Просто писать. Большое покрытие TDD @Test fun `when card number

    not starts with 2, 3, 6 - then show error`() { // Given val viewModel = CardAdditionViewModel() val visaCardNumber = "4567" // When viewModel.onCardNumberEntered(visaCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NOT_MIR_PAYMENT_SYSTEM_CARD) }
  21. Просто писать. Большое покрытие TDD @Test fun `when card number

    not starts with 2, 3, 6 - then show error`() { // Given val viewModel = CardAdditionViewModel() val visaCardNumber = "4567" // When viewModel.onCardNumberEntered(visaCardNumber) // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.NOT_MIR_PAYMENT_SYSTEM_CARD) }
  22. Просто писать. Большое покрытие TDD @Test fun `when user enter

    less then 16 digits - then add button should be disabled`() { // Given val viewModel = CardAdditionViewModel() val notFullyEnteredCardNumber = "12345" // When viewModel.onCardNumberEntered(notFullyEnteredCardNumber) // Then assertThat(viewModel.state.value!!.isAddCardButtonEnabled) .isFalse() }
  23. Просто писать. Большое покрытие TDD @Test fun `when user enter

    less then 16 digits - then add button should be disabled`() { // Given val viewModel = CardAdditionViewModel() val notFullyEnteredCardNumber = "12345" // When viewModel.onCardNumberEntered(notFullyEnteredCardNumber) // Then assertThat(viewModel.state.value!!.isAddCardButtonEnabled) .isFalse() }
  24. Просто писать. Большое покрытие TDD @Test fun `when user click

    button - and Luhn check incorrect - then show error and disable button`() { // Given val viewModel = CardAdditionViewModel() val incorrectCardNumber = "2111 2222 3333 4444" viewModel.onCardNumberEntered(incorrectCardNumber) // When viewModel.onAddClick() // Then assertThat(viewModel.state.value!!.cardNumberState.error) .isEqualTo(CardNumberError.LUHN_CHECK_INCORRECT) assertThat(viewModel.state.value!!.isAddCardButtonEnabled) .isFalse() }
  25. Просто писать. Большое покрытие Пишем тест на баг @Test fun

    `when error occurs - and user clicks retry button - then clear card number field`() { // Given val cardBindingError = MessageServerException() val viewModel = CardAdditionViewModel( bindCardUseCase = mock { on { bindCard(any()) }.thenReturn(Completable.error(cardBindingError)) }, ) // When val correctCardNumber = "2202 1118 4013 1368" viewModel.onCardNumberEntered(correctCardNumber) viewModel.onAddClick() viewModel.onRetryClick() // Then val currentState = viewModel.state.value!! assertThat(currentState.cardNumberState.number) .isBlank() assertThat(currentState.isAddCardButtonEnabled) .isFalse() }
  26. Просто писать. Большое покрытие Пишем тест на баг @Test fun

    `when error occurs - and user clicks retry button - then clear card number field`() { // Given val cardBindingError = MessageServerException() val viewModel = CardAdditionViewModel( bindCardUseCase = mock { on { bindCard(any()) }.thenReturn(Completable.error(cardBindingError)) }, ) // When val correctCardNumber = "2202 1118 4013 1368" viewModel.onCardNumberEntered(correctCardNumber) viewModel.onAddClick() viewModel.onRetryClick() // Then val currentState = viewModel.state.value!! assertThat(currentState.cardNumberState.number) .isBlank() assertThat(currentState.isAddCardButtonEnabled) .isFalse() } Тапнуть на привязку карты
  27. Просто писать. Большое покрытие Пишем тест на баг @Test fun

    `when error occurs - and user clicks retry button - then clear card number field`() { // Given val cardBindingError = MessageServerException() val viewModel = CardAdditionViewModel( bindCardUseCase = mock { on { bindCard(any()) }.thenReturn(Completable.error(cardBindingError)) }, ) // When val correctCardNumber = "2202 1118 4013 1368" viewModel.onCardNumberEntered(correctCardNumber) viewModel.onAddClick() viewModel.onRetryClick() // Then val currentState = viewModel.state.value!! assertThat(currentState.cardNumberState.number) .isBlank() assertThat(currentState.isAddCardButtonEnabled) .isFalse() } Привязать уже привязанную карту
  28. Просто писать. Большое покрытие Пишем тест на баг @Test fun

    `when error occurs - and user clicks retry button - then clear card number field`() { // Given val cardBindingError = MessageServerException() val viewModel = CardAdditionViewModel( bindCardUseCase = mock { on { bindCard(any()) }.thenReturn(Completable.error(cardBindingError)) }, ) // When val correctCardNumber = "2202 1118 4013 1368" viewModel.onCardNumberEntered(correctCardNumber) viewModel.onAddClick() viewModel.onRetryClick() // Then val currentState = viewModel.state.value!! assertThat(currentState.cardNumberState.number) .isBlank() assertThat(currentState.isAddCardButtonEnabled) .isFalse() } Тапнуть “попробовать снова”
  29. Просто писать. Большое покрытие Пишем тест на баг @Test fun

    `when error occurs - and user clicks retry button - then clear card number field`() { // Given val cardBindingError = MessageServerException() val viewModel = CardAdditionViewModel( bindCardUseCase = mock { on { bindCard(any()) }.thenReturn(Completable.error(cardBindingError)) }, ) // When val correctCardNumber = "2202 1118 4013 1368" viewModel.onCardNumberEntered(correctCardNumber) viewModel.onAddClick() viewModel.onRetryClick() // Then val currentState = viewModel.state.value!! assertThat(currentState.cardNumberState.number) .isBlank() assertThat(currentState.isAddCardButtonEnabled) .isFalse() } Поле ввода должно очиститься
  30. Сложно тестировать вручную Выносим код Redmadrobot class Greeting( currentTime: DateTime,

    private val userName: String? ) { // Ночь: 00:00 - 05:59 private val night: Interval = Interval(nightStart, morningStart) // Утро: 06:00 - 11:59 private val morning: Interval = Interval(morningStart, dayStart) // День: 12:00 - 17:59 private val day: Interval = Interval(dayStart, eveningStart) // Вечер: 18:00 - 23:59 private val evening: Interval = Interval(eveningStart, end) @StringRes val greetingRes: Int init { greetingRes = when { night.contains(currentTime) -> R.string.fragment_pin_input_greeting_night morning.contains(currentTime) -> R.string.fragment_pin_input_greeting_morning day.contains(currentTime) -> R.string.fragment_pin_input_greeting_day evening.contains(currentTime) -> R.string.fragment_pin_input_greeting_evening else -> throw IllegalStateException("Cant determine greeting for $currentTime") } } fun greet(context: Context): String { return context.getString(greetingRes) + if (userName != null) ",\n$userName" else "" } }
  31. Сложно тестировать вручную Пишем тест Redmadrobot @Test fun `when night

    - then show correct greeting`() { // Given // 04:20 val night: DateTime = DateTime().withTime(4, 20, 0, 0) // When val greeting = Greeting(night, username) // Then assertThat(greeting.greetingRes).isEqualTo(R.string.fragment_pin_input_greeting_night) val expectedResult = "Доброй ночи,\n$username" assertThat(greeting.greet(contextMock)).isEqualTo(expectedResult) }
  32. Сложно тестировать вручную Пишем тест Redmadrobot @Test fun `when night

    - then show correct greeting`() { // Given // 04:20 val night: DateTime = DateTime().withTime(4, 20, 0, 0) // When val greeting = Greeting(night, username) // Then assertThat(greeting.greetingRes).isEqualTo(R.string.fragment_pin_input_greeting_night) val expectedResult = "Доброй ночи,\n$username" assertThat(greeting.greet(contextMock)).isEqualTo(expectedResult) }
  33. Сложно тестировать вручную Ошибки прячутся на границах Redmadrobot @Test fun

    `when night end - then show correct greeting`() { // Given // 05:59 val night: DateTime = DateTime().withTime(5, 59, 0, 0) // When val greeting = Greeting(night, username) // Then assertThat(greeting.greetingRes).isEqualTo(R.string.fragment_pin_input_greeting_night) val expectedResult = "Доброй ночи,\n$username" assertThat(greeting.greet(contextMock)).isEqualTo(expectedResult) }
  34. Сложно тестировать вручную Ошибки прячутся на границах Redmadrobot @Test fun

    `when night end - then show correct greeting`() { // Given // 05:59 val night: DateTime = DateTime().withTime(5, 59, 0, 0) // When val greeting = Greeting(night, username) // Then assertThat(greeting.greetingRes).isEqualTo(R.string.fragment_pin_input_greeting_night) val expectedResult = "Доброй ночи,\n$username" assertThat(greeting.greet(contextMock)).isEqualTo(expectedResult) }
  35. Сложная логика Шифрование Redmadrobot Процедура шифрования: 1. Получаем из определенного

    URL публичный ключ для асиметричного шифрования в формате [XXXX]
 2. Генерируем [ДАННЫЕ УТЕРЯНЫ] ключ для генерации сообщения (длина - [СЕКРЕТНО] бит)
 3. Шифруем JSON симметричным ключом с помощью алгоритма AES/CBC/PKCS5Padding
 4. Шифруем симметричный ключ с помощью публичного ключа используя алгоритм RSA/ECB/ OAEPWithSHA-256AndMGF1Padding
 5. В первый блок пакета включаем шифрованный симметричный ключ, второй блок содержит случайно сгенерированный initialization vector, в остальных блоках передается [УДАЛЕНО]
 6. Весь пакет кодируется [СОВЕРШЕННО СЕКРЕТНО]
  36. Сложная логика Выносим код Redmadrobot class BindCardUseCase( private val cardsRepository:

    CardsRepository, private val cardNumberCipher: CardNumberCipher ) { fun bindCard(cardNumber: String): Completable { return cardsRepository .map { key -> encryptCardNumber(cardNumber, key) } .flatMapCompletable { encryptedNumber -> cardsRepository.bindCard(encryptedNumber) } } } interface CardNumberCipher { fun encrypt(cardNumber: String, key: PublicKey): String }
  37. Сложная логика Выносим код Redmadrobot class BindCardUseCase( private val cardsRepository:

    CardsRepository, private val cardNumberCipher: CardNumberCipher ) { fun bindCard(cardNumber: String): Completable { return cardsRepository .map { key -> encryptCardNumber(cardNumber, key) } .flatMapCompletable { encryptedNumber -> cardsRepository.bindCard(encryptedNumber) } } } interface CardNumberCipher { fun encrypt(cardNumber: String, key: PublicKey): String }
  38. Сложная логика Убираем android-зависимости Redmadrobot class CardNumberCipherImpl(private val base64: (ByteArray)

    -> String) : CardNumberCipher { override fun encrypt(cardNumber: String, key: PublicKey): String { // Генерируем симметричный ключ val aesKeyGenerator = KeyGenerator.getInstance("AES") aesKeyGenerator.init() val aesKey = aesKeyGenerator.generateKey() cardNumberCipher.init(Cipher.ENCRYPT_MODE, aesKey) val iv: ByteArray = cardNumberCipher.iv // Шифруем JSON симметричным ключом val rawJson = """{"pan":"$cardNumber"}""" val aesEncryptedJson = cardNumberCipher.doFinal(rawJson.toByteArray()) // Шифруем симметричный ключ с помощью публичного ключа symmetricKeyCipher.init(Cipher.WRAP_MODE, key, oaepParams) val rsaEncryptedAesKey: ByteArray = symmetricKeyCipher.wrap(aesKey) val resultPackage = rsaEncryptedAesKey // Весь пакет кодируется base64 return base64(resultPackage) } }
  39. Сложная логика Убираем android-зависимости Redmadrobot class CardNumberCipherImpl(private val base64: (ByteArray)

    -> String) : CardNumberCipher { override fun encrypt(cardNumber: String, key: PublicKey): String { // Генерируем симметричный ключ val aesKeyGenerator = KeyGenerator.getInstance("AES") aesKeyGenerator.init() val aesKey = aesKeyGenerator.generateKey() cardNumberCipher.init(Cipher.ENCRYPT_MODE, aesKey) val iv: ByteArray = cardNumberCipher.iv // Шифруем JSON симметричным ключом val rawJson = """{"pan":"$cardNumber"}""" val aesEncryptedJson = cardNumberCipher.doFinal(rawJson.toByteArray()) // Шифруем симметричный ключ с помощью публичного ключа symmetricKeyCipher.init(Cipher.WRAP_MODE, key, oaepParams) val rsaEncryptedAesKey: ByteArray = symmetricKeyCipher.wrap(aesKey) val resultPackage = rsaEncryptedAesKey // Весь пакет кодируется base64 return base64(resultPackage) } }
  40. Сложная логика Где-то в DI Redmadrobot @Provides @AppScope internal fun

    provideCardNumberCipher(): CardNumberCipher { return CardNumberCipherImpl( { bytes: ByteArray -> Base64.encodeToString(bytes, Base64.NO_WRAP) } ) }
  41. Сложная логика Пишем тест Redmadrobot @Test fun `when encrypt data

    - then decrypt works correctly`() { // Given val cipher = CardNumberCipherImpl(Base64.getEncoder()::encodeToString) // When val cardNumber = "2200111552975178" val keyPair: KeyPair = KeyPairGenerator.getInstance("RSA").genKeyPair() val encryptedData = cipher.encrypt(cardNumber, keyPair.public) // Then // Разбираем исходный пакет val base64Decoded = Base64.getDecoder().decode(encryptedData) val encryptedAesKey = ByteArray(AES_KEY_LENGTH) System.arraycopy(base64Decoded, 0, encryptedAesKey, 0, AES_KEY_LENGTH) val iv = ByteArray(IV_LENGTH) System.arraycopy(base64Decoded, AES_KEY_LENGTH, iv, 0, IV_LENGTH) val encryptedDataLength = base64Decoded.size - AES_KEY_LENGTH - IV_LENGTH val aesEncryptedData = ByteArray(encryptedDataLength) System.arraycopy(base64Decoded, AES_KEY_LENGTH + IV_LENGTH, aesEncryptedData, 0, encryptedDataLength) // Расшифруем ключ для AES-шифрования приватным ключом RSA val symmetricKeyCipher = Cipher.getInstance(CardNumberCipherImpl.SYMMETRIC_KEY_ENCRYPTION_ALGORITHM) symmetricKeyCipher.init(Cipher.UNWRAP_MODE, keyPair.private, CardNumberCipherImpl.oaepParams) val aesKey = symmetricKeyCipher.unwrap(encryptedAesKey, "AES", Cipher.SECRET_KEY) // Расшифруем данные с помощью расшифрованного AES-ключа val cardNumberCipher = Cipher.getInstance(CardNumberCipherImpl.CARD_NUMBER_ENCRYPTION_ALGORITHM) cardNumberCipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv)) val resultedDecryptedData = String(cardNumberCipher.doFinal(aesEncryptedData)) val expectedJson = """{"pan":"$cardNumber"}""" assertThat(resultedDecryptedData).isEqualTo(expectedJson) }
  42. Менять код без страха Когда хочется эксперимента Redmadrobot @Test fun

    `when deserialize company json - then create correct object`() { // Given val companyJson = """ { "id": 52000000030, "name": "Wall Street English", "description": "Школа английского языка", "detailedDescription": "Школа Wall Street English является передовой", "companyAvatar": [ { "url": "https://1047016741.rsc.cdn77.org/pp052/normal/52000000030_f648420611.jpg", "width": 240, "height": 240 } ] } """.trimIndent() // When val company = fromJson<Company>(companyJson) // Then val expectedImage = Image( url = "https://1047016741.rsc.cdn77.org/pp052/normal/52000000030_f648420611.jpg", width = 240, height = 240 ) assertThat(company).isNotNull with(company!!) { assertThat(id).isEqualTo(52000000030L) assertThat(name).isEqualTo("Wall Street English") assertThat(description).isEqualTo("Школа английского языка") assertThat(companyAvatar).containsExactly(expectedImage) } } Moshi
  43. Менять код без страха Зачем я написал тесты на Json

    Redmadrobot 01 02 03 Нет уверенности в библиотеке, поэтому хочется подстраховаться на случай возврата на Gson Добавление адаптеров не должно сломать сериализацю/десериализацию Тесты - документация на библиотеку 04 Проще фиксить баги с обязательностью/необязательностью полей
  44. Менять код без страха Один assert на тест? Redmadrobot with(company!!)

    { assertThat(id).isEqualTo(52000000030L) assertThat(name).isEqualTo("Wall Street English") assertThat(description).isEqualTo("Школа английского языка") assertThat(companyAvatar).containsExactly(expectedImage) }
  45. Менять код без страха Код, который постоянно меняется или может

    поменяться Redmadrobot fun Double.formatAsMoney(): String { return "${this.formatValue()} руб." } private fun Double.formatValue(): String { val cashbackFormat = if (this % 1.0 == 0.0) "%.0f" else "%.2f" return String.format(Locale.US, cashbackFormat, this) }
  46. Менять код без страха Пишем тест Redmadrobot class DoubleExtTest {

    @Test fun `when formatting raw cashback with decimal value - then result is correct`() { // Given val rawCashBackValue = 3535.7 // When val result = rawCashBackValue.formatAsMoney() // Then assertThat(result).isEqualTo("3535.70 руб.") } @Test fun `when formatting raw cashback without decimal value - then result is correct`() { // Given val rawCashBackValue = 42.0 // When val result = rawCashBackValue.formatAsMoney() // Then assertThat(result).isEqualTo("42 руб.") } }
  47. Менять код без страха Валидация Redmadrobot object CardholderNameValidator { private

    const val MAX_CARDHOLDER_NAME_LENGTH = 19 // Регулярка для проверки требований к имени (2 слова) private val namePattern: Pattern = Pattern.compile("""[\-.’A-Z]+[\s][\-.’A-Z]+$""") fun isValidCardholderName(name: String) = (name.length in 3..MAX_CARDHOLDER_NAME_LENGTH) && (namePattern.matcher(name).matches()) }
  48. Менять код без страха Параметрические тесты Redmadrobot @RunWith(Parameterized::class) class CardholderNameValidatorTest(private

    val inputName: String, private val expectedResult: Boolean) { companion object { @JvmStatic @Parameterized.Parameters fun data(): Collection<Array<Any>> { return listOf( arrayOf("IVAN IVANOV", true), arrayOf("IVAN IVANOV-PETROV", true), arrayOf("IVANIVANOV", false), arrayOf("!V@№ !v@№0v", false), arrayOf("IVANIVAN IVANOVIVANOV", false), arrayOf(" I ", false), arrayOf("II", false), arrayOf("II VV AA", false) ) } } @Test fun `cardholder name should be valid`() { assertThat(CardholderNameValidator.isValidCardholderName(inputName)) .isEqualTo(expectedResult) } }