Em nossos projetos, tentamos cobrir o código com testes conforme necessário e aderir aos princípios de SOLID e arquitetura limpa. Gostaríamos de compartilhar com os leitores da Habr a tradução do artigo de Hannes Dorfmann, autor de uma série de publicações sobre o desenvolvimento do Android. Este artigo descreve uma técnica que pode ajudá-lo a abstrair o trabalho com strings para ocultar os detalhes da interação com diferentes tipos de recursos de string e tornar mais fácil escrever testes de unidade.
Se você está trabalhando em um grande aplicativo Android e suspeita que seu código pode ficar confuso ao trabalhar com recursos de fontes diferentes, ou se deseja simplificar a escrita de testes em strings, este artigo pode ser útil para você. Traduzido com permissão do autor.
. , Android Android.
?
Android? , , . , , , , , , . , :
R.string.some_text, resources.getString(R.string.some_text)
, , .. context.getString(R.string.some_text, «arg1», 123)
<string name=”some_formatted_text”>Some formatted Text with args %s %i</string>
, Plurals, , resources.getQuantityString(R.plurals.number_of_items, 2):
<plurals name="number_of_items">
<item quantity="one">%d item</item>
<item quantity="other">%d items</item>
</plurals>
, Android XML- strings.xml, String ( R.string.some_text). , , json .
, , ? . :
1. , , .
2. ( ) -, , .
: , http, , fallback- strings.xml. , :
class MyViewModel(
private val backend : Backend,
private val resources : Resources // Android context.getResources()
) : ViewModel() {
val textToDisplay : MutableLiveData<String> // MutableLiveData
fun loadText(){
try {
val text : String = backend.getText()
textToDisplay.value = text
} catch (t : Throwable) {
textToDisplay.value = resources.getString(R.string.fallback_text)
}
}
}
MyViewModel, . , loadText(), Resources, StringRepository ( ""), :
interface StringRepository{
fun getString(@StringRes id : Int) : String
}
class AndroidStringRepository(
private val resources : Resources // Android context.getResources()
) : StringRepository {
override fun getString(@StringRes id : Int) : String = resources.getString(id)
}
class TestDoubleStringRepository{
override fun getString(@StringRes id : Int) : String = "some string"
}
- StringRepository , , ?
class MyViewModel(
private val backend : Backend,
private val stringRepo : StringRepository //
) : ViewModel() {
val textToDisplay : MutableLiveData<String>
fun loadText(){
try {
val text : String = backend.getText()
textToDisplay.value = text
} catch (t : Throwable) {
textToDisplay.value = stringRepo.getString(R.string.fallback_text)
}
}
}
- -:
@Test
fun when_backend_fails_fallback_string_is_displayed(){
val stringRepo = TestDoubleStringRepository()
val backend = TestDoubleBackend()
backend.failWhenLoadingText = true // backend.getText()
val viewModel = MyViewModel(backend, stringRepo)
viewModel.loadText()
Assert.equals("some string", viewModel.textToDisplay.value)
}
interface StringRepository , ? . , :
StringRepository , (. ). , - , , String. .
, TestDoubleStringRepository , , ? TestDoubleStringRepository . -, R.string.foo R.string.fallback_text StringRepository.getString(), . , TestDoubleStringRepository, :
class TestDoubleStringRepository{
override fun getString(@StringRes id : Int) : String = when(id){
R.string.fallback_test -> "some string"
R.string.foo -> "foo"
else -> UnsupportedStringResourceException()
}
}
? ( )?
, .
TextResource
TextResource. , domain. , -. :
sealed class TextResource {
companion object { // ,
fun fromText(text : String) : TextResource = SimpleTextResource(text)
fun fromStringId(@StringRes id : Int) : TextResource = IdTextResource(id)
fun fromPlural(@PluralRes id: Int, pluralValue : Int) : TextResource = PluralTextResource(id, pluralValue)
}
}
private data class SimpleTextResource( // inline
val text : String
) : TextResource()
private data class IdTextResource(
@StringRes id : Int
) : TextResource()
private data class PluralTextResource(
@PluralsRes val pluralId: Int,
val quantity: Int
) : TextResource()
//
...
- TextResource:
class MyViewModel(
private val backend : Backend // , , , - , StringRepository.
) : ViewModel() {
val textToDisplay : MutableLiveData<TextResource> // String
fun loadText(){
try {
val text : String = backend.getText()
textToDisplay.value = TextResource.fromText(text)
} catch (t : Throwable) {
textToDisplay.value = TextResource.fromStringId(R.string.fallback_text)
}
}
}
:
1) textToDisplay c LiveData<String> LiveData<TextResource>, - , String. TextResource. , , , TextResource – , .
2) -. « » StringRepository ( Resources). , , , ? , TextResource. , Android, (R.string.fallback_text – Int). -:
@Test
fun when_backend_fails_fallback_string_is_displayed(){
val backend = TestDoubleBackend()
backend.failWhenLoadingText = true // backend.getText()
val viewModel = MyViewModel(backend)
viewModel.loadText()
val expectedText = TextResource.fromStringId(R.string.fallback_text)
Assert.equals(expectedText, viewModel.textToDisplay.value)
// data class- equals,
}
, : TextResource String, , , TextView? , Android, UI.
// context.getResources()
fun TextResource.asString(resources : Resources) : String = when (this) {
is SimpleTextResource -> this.text // smart cast
is IdTextResource -> resources.getString(this.id) // smart cast
is PluralTextResource -> resources.getQuantityString(this.pluralId, this.quantity) // smart cast
}
TextResource String UI ( ) , TextResource «» (.. ), R.string.* .
: - TextResource.asString(), . , when. resources.getString(). , TextResource , «/». , , : , TextResource, when TextResource.asString().
: , TextResource /. / TextResource, sealed class TextResouce abstract fun asString(r: Resources), . , / asString(r: Resources), ( , , /). ? , Resources API TextResource , (, SimpleTextResource ). , API, , ( ).
: dimens, , . , . , – , , . !