더이상 리소스가 필요하지 않을 때, close 메소드를 활용하여 명시적으로 닫아야하는 리소드들이 있다.
코트린/JVM에서 사용하는 이런 리소스가 굉장히 많은데 대표적으론 다음과 같다.
이러한 리소스들은 AutoCloseable
을 상속받는 Closeable
인터페이스를 구현하고 있다. 이러한 모든 리소스는 최종적으로 리소스에 대한 레퍼런스가 없어질 때 가비지 컬렉터가 처리해주지만, 느리며 그동안 리소스 유지비용이 많이 들어간다. 따라서 더 이상 필요하지 않을 때 close
메소드를 호출하여 명시적으로 리소스를 반환하는 것이 국룰이다.
코틀린에서는 표준 라이브러리에 use라는 확장함수를 가지고 있는데 이를 사용하여 자동으로 리소스를 반환 할 수 있다.
fun countCharactersInFile(path : File) : Int {
val reader = BufferdReader(FileReader(path))
readers.use{
return reader.lineSequence().sumBy { it.length }
}
}
context.contentResolver.query.use {
//...
}
파일을 리소스로 사용하는 경우가 많고, 파일을 한 줄씩 읽어 들이는 경우도 많음으로 코틀린에서는 파일을 한 줄씩 처리할 때 활용할 수 있는 useLines함수도 제공한다.
fun countCharactersInFile(path : File) : Int {
File(path).useLines { lines ->
return lines.sumBy { it.length }
}
or
File(path).useLines { lines ->
lines.sumBy { it.length }
}
}
이렇게 사용하면 메모리에 파일의 내용을 한 줄씩만 유지하므로, 대용량 파일도 적절하게 처리할 수 있다.
use를 사용하면 Closeable/AutoCloseable을 구현한 객체를 쉽고 안전하게 처리할 수 있다.
파일을 처리할 때는 파일을 한 줄씩 읽어 들이는 useLines를 사용하는 것이 좋다.
코드를 안전하게 만드는 가장 궁극적인 방법은 다양한 종류의 테스트를 하는 것이다.
단위 테스트는 개발자가 작성하며, 개발자에게 유효하다. 어플리케이션 외부적이 아닌 내부적으로 잘 작동하는 확인하는 것이다.
다음과 같은 예를 보자
@Test
fun `fib works corretly for ther first 5 positions`(){
assertEquals(1, fib(0))
assertEquals(1, fib(1))
assertEquals(3, fib(2))
assertEquals(4, fib(3))
assertEquals(5, fib(4))
}
Int
형에 Int.MaX_VALUE
가 들어가는 경우, nullable
인데 null
또는 null값으로 채워진 객체
가 들어가는 경우 등단위 테스트는 개발자가 만들고 있는 요소가 제대로 동작하는지를 빠르게 피드백해주므로 개발하는 동안 큰 도움이 된다.
테스트는 계속해서 축적되므로, 회귀 테스트도 쉽다.
수동으로 테스트하기 어려운 것들도 확인할 수 있다.
테스트 작성의 장점
단점
단위 테스트를 만드는데 시간이 걸린다.
그러나 장기적으로 좋은 단위 테스트는 디버깅 시간
과 버그를 찾는데 소요도는 시간
을 줄여준다. 수동 테스트보다 훨씬빠르고 시간이 절약된다.
테스트를 활용할 수 있게 코드를 조정해야 한다. 기존의 소스를 변경하기 어렵긴 하지만 이러한 변경으로 훌륭하고 잘 정립된 아키텍처를 사용하는 것이 강제된다.
좋은 단위 테스트를 만드는 것이 어렵다. 남은 개발 과정에 대한 확실한 이해가 필요하다. 잘못 만들어진 단위 테스트는 득보다 실이 크다.
메소드이름_
테스트중인상태_
예상되는결과
example: isAdult_AgeLessThan18_False
메소드이름_
예상결과_
테스트중인상태
example: isAdult_False_AgeLessThan18
test
테스트를 수행하는 상태(특징)
example: testIsNotAnAdultIfAgeLessThan18
테스트를 수행하는 상태(특징) = FeatureToBeTested
example: IsNotAnAdultIfAgeLessThan18
Should_
예상 결과값_
When_
테스트 중인 상태
example: Should_ThrowException_When_AgeLessThan18
When_
테스트 중인 상태_
예상 결과
example: When_AgeLessThan18_Expect_isAdultAsFalse
Given_
초기 상태_
When_
테스트하는 상황_
Then_
예상결과
example: Given_UserIsAuthenticated_When_InvalidAccountNumberIsUsedToWithdrawMoney_Then_TransactionsWillFail
메소드이름
_
테스트중인상태_
예상되는결과
를 사용
컨벤션을 활용하여 테스트를 진행해 보겠다.
테스트를 만들기 위해서는 해당 Class -> Generate -> Test를 활용해 자동으로 테스트클래스를 만들 수 있다.
class GalleryViewModelTest {
@Before
fun setUp() {
// 테스트가 실행되기 전에 실행된다.
// 초기화 할 변수, 상태 등을 정의한다.
}
@After
fun tearDown() {
// 테스트가 끝난 후 실행된다.
}
}
다음은 JUnit4를 사용하여 뷰모델의 로직을 체크하는 단위테스트이다.
@RunWith(AndroidJUnit4::class)
class GalleryViewModelTest {
private val _compositionTags =
MutableStateFlow<MutableList<Pair<String, String>>>(mutableListOf())
val compositionTags: StateFlow<MutableList<Pair<String, String>>>
get() = _compositionTags.asStateFlow()
@Before
fun setUp() {
}
@After
fun tearDown() {
}
/** 해당함수는 파라미터로 들어온
* 동일한 Pair객체가 있으면 삭제하고, 없으면 추가하여 _compositinoTags의 값으로 반환한다. */
fun handleUpdateCompositionTags(pair: Pair<String, String>) {
val temp = _compositionTags.value
if (temp.contains(pair)) {
temp.remove(pair)
} else {
temp.add(pair)
}
_compositionTags.value = temp
}
@Test
fun `handleUpdateCompositionTags`_`isDuplicated`_`retrunFalse`() {
val test1 = Pair("A", "B")
handleUpdateCompositionTags(test1)
handleUpdateCompositionTags(test1)
assertThat(compositionTags.value.contains(test1)).isEqualTo(false)
}
}
import com.google.common.truth.Truth.assertThat
를 사용하였으며,
Truth 는 Guava 팀에서 개발한 Assertion 테스팅 라이브러리중 하나로, 지금까지 사용했던 Junit4 대신에 사용할 수 있으며 다양한 기능을 제공하여 효율적으로 테스팅 코드를 만들 수 있다.
@RunWith(AndroidJUnit4::class)
@Config(sdk = [Q]) // API 29를 타겟으로 테스트
class TokenRepositoryImplTest {
lateinit var dataStore: DataStore<Preferences>
private val context: Context = ApplicationProvider.getApplicationContext()
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
dataStore = PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile(DATASTORE_NAME) }
)
}
@After
fun tearDown() {
}
suspend fun setData(value: String) {
dataStore.edit { prefs ->
prefs[stringPreferencesKey("TOKEN_KEY_TEST")] = value
}
}
suspend fun getData(): Flow<String> =
dataStore.data.catch { exception ->
if (exception is IOException) {
exception.printStackTrace()
emit(emptyPreferences())
} else {
throw exception
}
}.map { prefs ->
prefs[stringPreferencesKey("TOKEN_KEY_TEST")] ?: ""
}
@Test
@ExperimentalCoroutinesApi
fun `setDataAndGetData_saveAndGetAction_equals`() = runTest {
setData("TEST_TOKEN")
var token = getData().first()
assertEquals(token, "TEST_TOKEN")
}
}
private val context: Context = ApplicationProvider.getApplicationContext()
를 활용하여 Application의 Context를 가져올 수 있다.
JUni4의 Assert를 사용하여 테스트를 진행하였다.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
위에 설정한 규칙은 백그라운드 작업과 연관된 모든 아키텍처 컴포넌트들을 같은(한개의) 스레드에서 실행되게 해서 테스트 결과들이 동기적으로 실행되게 하게 해준다.
@ExperimentalCoroutinesApi
및 runTest
를 이용하여 테스트가 백그라운드의 testscope에서 진행되게 한다.
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
Fake Repository를 이용하여 ViewModel 로직을 실험해보자
class FakeCloverRepository {
fun getClovers() = flow {
emit(
GetCloverResponse(
listOf<CloverCount>(
CloverCount(1, "Turn"),
CloverCount(1, "ManyYears")
),
5,
listOf(
Ratio("Turn", 50),
Ratio("ManyYears", 50)
)
)
)
}
}
서버에서오는 GetCloverResponse
라는 형태의 데이터 구조를 가지고 임의의 값을 넣어서 flow
로 반환하는 가짜 형태의 서버 데이터를 생성한다.
@ExperimentalCoroutinesApi
class CloverViewModelTest {
private val repository = FakeCloverRepository()
@Test
fun `getClovers_onInit_convertToPieChartData`() = runTest {
repository.getClovers().collectLatest {
val testData = setpieChartList(it.ratios)
assertThat(testData.size).isEqualTo(2)
val testData2 = setRataioList(it.cloverCounts)
println(testData2.toString())
// Tests..
}
}
}
이를 이용하여 실제로 받아온 데이터처럼 이용해 값이 정상적으로 처리되는지 확인한다.
Kotlin에서의 UnitTest에서는 println()
을 사용하여 값을 중간에 체크할 수 있다.
Turbine
이라는 라이브러리를 사용하는것인데, 이는 kotlinx.coroutines의 Flow 테스팅 라이브러리다.
dependencies {
testImplementation 'app.cash.turbine:turbine:0.9.0'
}
someFlow.test{}
의 형식으로 간단하게 테스트를 진행할 수 있으며 test람다 내의 flow는 차례대로한번씩 소비된다.
flowOf("one", "two").test {
assertThat("one").isEqualTo(awaitItem()) // True
// Flow 소비 안함 error
}
테스트 블록 내에서 흐름에서 수신된 모든 Flow를 소비해야하며, 그렇지 않으면 테스트에 실패한다.
@Test
fun `turbineTest`() = runTest {
flowOf("one", "two").test {
assertThat("one").isEqualTo(awaitItem())
assertThat("two").isEqualTo(awaitItem())
awaitComplete()
}
} // Test Complete!
turbine
내의 test람다 내에서는 수많은 함수를 지원한다.
cancelAndIgnoreRemainingEvents
: 캔슬하고 남은 이벤트를 모두 무시한다.cancel
: 현재 Flow 캔슬expectMostRecentItem
: 가장 최근의 Flow를 할당받는다.Flow를 중간에 개발자가 다룰 수 있게 만들어 놨다.
이를 활용해
같은 Scope내에서 Flow들의 처리
@Test
@ExperimentalCoroutinesApi
fun turbineTest() = runTest {
val turbine1 = flowOf(1).testIn(this)
val turbine2 = flowOf(2).testIn(this)
assertEquals(1, turbine1.awaitItem())
assertEquals(2, turbine2.awaitItem())
turbine1.awaitComplete()
turbine2.awaitComplete()
}
에러, 실패를 Flow로 처리, Hot Flows 처리, 비동기 Flow처리까지 자세한 건 turbine-github를 참고하도록 하자.
참고 자료
https://youngest-programming.tistory.com/m/492