일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 안드로이드 테스트코드
- 안드로이드 mvvm
- RxJava
- 안드로이드 앱 아키텍처 가이드라인 설명
- 안드로이드 리싸이클러뷰
- 안드로이드 JUnit
- 안드로이드 mvvm예제
- 안드로이드 앱 아키텍처 가이드라인
- Android MVVM
- 안드로이드 의존성주입
- 리싸이클러뷰 최적화
- 안드로이드 아키텍처 컴포넌트
- Android App Architecture Guideline
- android memory leak
- 안드로이드 Mockito
- 안드로이드 클린 아키텍처
- android clean architecture
- MVVM
- Hilt
- sharedFlow
- 스타트업 코딩테스트
- coroutine
- 안드로이드 hilt
- 안드로이드 Espresso
- Koin
- 코루틴
- android DI
- 안드로이드 앱 아키텍처 가이드라인 사용법
- 안드로이드 최적화
- 안드로이드 앱 아키텍처 가이드라인 예시
- Today
- Total
안드로이드 연구소
[연봉 5000 넘는 안드로이드 개발자 되는 법] Unit테스트란(ViewModel에서 Unit테스트하기) 본문
[연봉 5000 넘는 안드로이드 개발자 되는 법] Unit테스트란(ViewModel에서 Unit테스트하기)
안드로이드 연구원 2023. 5. 21. 14:01저번 강의에서 안드로이드 테스트코드의 종류들을 살펴보았습니다.
그 중 첫번째 핵심 테스트인 Unit테스트를 시작해보겠습니다.
Q1. 안드로이드에서 사용한 Unit테스트에 대해서 설명해줘.
안드로이드 단위테스트는 함수나 컴포넌트가 격리된 상태(isolation)에서 올바르게 작동하는지 테스트하는 방식을 의미합니다.
이러한 테스트는 일반적으로 JVM(Java Virtual Machine)을 사용하여 작성 및 실행하여
물리적 장치나 에뮬레이터에 배포하지 않고도 테스트를 실행할 수 있습니다.
안드로이드의 단위 테스트는 일반적으로 JUnit 또는 Mockito와 같은 테스트 프레임워크를 사용하여 작성됩니다.
이전 게시물에서 Instrumentation테스트(즉, 안드로이드 테스트)는 디바이스나 에뮬레이터에서 테스트하는 방법이라고 하였습니다.
그렇다면 그 반대인 Unit테스트는 JVM위에서 테스트를 실행합니다.
그리고 JUnit과 Mockito같은 프레임워크들이 있습니다.
Q2. JUnit에 대해 설명해줘.
안드로이드 뿐만 아니라 Java 기반 애플리케이션에 널리 사용되는 테스트 프레임워크입니다.
annotations, assertions을 사용하여 테스트 코드를 작성한다.
1. Annotations: 테스트를 정의하고 구성하는데 사용됩니다.
- @Test: 테스트 대상인 테스트 케이스로 표시
- @Before: @Test로 표시된 테스트 케이스가 실행되는 동작. 객체 초기화같은 작업들을 한다.
- @After: @Test로 표시된 테스트 케이스가 실행되는 동작. 정리(clean up) 또는 해제(executed)같은 작업을 한다..
- @BeforeClass: 테스트 클래스 중 가장 먼저 실행되는 메서드에 지정합니다.
- @AfterClass: 테스트 클래스중 가장 늦게 실행되는 메서드에 지정합니다.
2. Assertions: 실제 값을 예상 값과 비교하여 검증하는데 사용되는 메서드입니다.
- assertEquals(), assertTrue(), assertFalse(), assertNotNull() 등이 있습니다.
3. 테스트 실행: JUnit은 테스트 Runner를 사용하여 테스트 사례를 실행하고 보고서를 생성합니다.
@AndroidJUnitRunner와 같은 어노테이션이 있습니다.
JUnit도 Dagger와 Hilt처럼 어노테이션을 사용하는 프레임워크인 것 같습니다.
이해가 안된다면 예제로 확인해보겠습니다.
(따라하진 마시고 어떻게 사용하는 정도만 확인해주시면 됩니다.)
dependencies {
testImplementation 'junit:junit:$version'
androidTestImplementation 'androidx.test.ext:junit:$version'
}
JUnit은 기본적으로 안드로이드 스튜디오 파일 세팅하면 자동으로 추가되어있다.
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.Assert
class ExampleUnitTest {
private var counter: Int = 0
@Before //(1)
fun setUp() {
// This method will be executed before each test method
counter = 0
println("Before Test - Setting up the test")
}
@After //(4)
fun clear() {
// This method will be executed after each test method
println("After Test - Cleaning up the test")
}
@Test //(2)
fun incrementCounter() {
counter++
Assert.assertEquals(1, counter)
}
@Test //(3)
fun decrementCounter() {
counter--
Assert.assertEquals(-1, counter)
}
}
위 ExampleUnitTest 클래스에는 4가지 메서드가 있습니다.
2개의 메서드가 각 각@Before, @After 어노테이션을 사용하고 있고
두개의 메서드가 @Test어노테이션에 사용되고 있습니다.
(1)가장 먼저 실행되는 함수는 @Before 어노테이션을 사용하고 있는
setUp()메서드입니다.
(2)(3)그 다음으로 @Test어노테이션을 사용중인
incrementCounter()메서드와 decrementCounter()메서드가 테스트할 대상입니다.
Assert.assertEquals(1, counter)을 통해서 counter의 값이 1인지를 확인하고
Assert.assertEquals(-1, counter)으로 -1인지를 확인합니다.
(4)마지막으로 @After 어노테이션을 사용중인
clear()메서드가 실행되어 테스트를 마칩니다.
앞으로 JUnit은 유닛테스트를 할 때 뿐만 아니라 Instrumentation테스트를 사용하든지 사용하게될 베이스가 됩니다.
저의 말로 풀어서 해석하자면 테스트코드의 생명주기 역할과 채점 검사를 하는 답안지 역할을 해줄 것 입니다.
Q3. Mockito에 대해서 설명해줘.
Java 및 Kotlin을 위한 인기 있는 모킹 프레임워크로 mock 객체를 만들어낼 수 있다.
mock 객체는 실제 개체의 동작을 모방하지만 테스트 목적으로 제어 및 구성할 수 있는 시뮬레이션된 개체입니다.
Mockito를 사용하면 메서드 호출에 대한 반환 값 정의 또는 예외 발생과 같은 mock개체의 동작을 지정할 수 있습니다.
@Mock, @Spy, @InjectMocks와 같은 주석을 제공하여 테스트 내에서 모의 객체의 설정 및 사용을 단순화합니다.
Mockito.verify()메서드를 통해 테스트 대상을 검증할 수 있습니다.
Mockito는 JUnit과 잘 통합되며 JUnit 테스트와 함께 쉽게 사용할 수 있습니다.
Mockito 테스트 프레임워크에 가장 핵심은 mock객체인 것 같습니다.
mock은 흉내낸다는 뜻을 가진 단어인데 어떤 의미로 쓰이고 있는지 확인해보겠습니다.
Mockito도 JUnit처럼 어노테이션을 사용하는 것으로 보이네요.
테스트코드 파일 생성 및 실행하는 방법
https://developer.android.com/studio/test/test-in-android-studio?hl=ko
예제로 아래와 같이 있습니다.
최신버전: https://github.com/mockito/mockito/releases/
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
fun multiply(a: Int, b: Int): Int {
return a * b
}
}
위 Calculartor클래스의 add함수와 multiply함수는 테스트가 필요한 메서드들입니다.
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.Assert
class ExampleUnitTest {
private var counter: Int = 0
@Before //(1)
fun setUp() {
// This method will be executed before each test method
counter = 0
println("Before Test - Setting up the test")
}
@After //(4)
fun clear() {
// This method will be executed after each test method
println("After Test - Cleaning up the test")
}
@Test //(2)
fun incrementCounter() {
counter++
Assert.assertEquals(1, counter)
}
@Test //(3)
fun decrementCounter() {
counter--
Assert.assertEquals(-1, counter)
}
}
class CalculatorTest {
@Mock // (1)
lateinit var calculator: Calculator
@Before
fun setup() {
// (2)
MockitoAnnotations.initMocks(this)
}
@Test
fun testAdd() {
`when`(calculator.add(2, 3)).thenReturn(5) // (3)
val result = calculator.add(2, 3)// reulst = 5
}
//(4)
@Test
fun testMultiply() {
`when`(calculator.multiply(2, 3)).thenReturn(6)
val result = calculator.multiply(2, 3) // reulst = 6
}
}
(1)메서드들을 테스트하기위해 CalculatorTest 클래스를 생성합니다.
@Mock어노테이션 Calculator클래스의 mock객체를 만들어줍니다.
(2)또 테스트 전 실행되는 @Before JUnit 어노테이션에서
MockitoAnnotations.initMocks(this)를 사용하여 Mockito 어노테이션을 초기화합니다.
(3)@Test어노테이션을 사용하고 있는 testAdd() 메서드에서
Mockito의 when 메서드를 사용하여 mock 객체의 동작을 정의합니다.
`when`(mockObject.methodCall()).thenReturn(returnValue)
위처럼 when메서드를 사용하여 mockObject.methodCall()이 메서드의 반환값을 returnValue로 지정할 수 있습니다.
그리하여 calculator.add(2, 3)의 반환값을 5로 지정하게되는 것입니다.
(4)testMultiply함수도 똑같이 테스트합니다.
그런데 굳이 Mock객체를 만들어서 테스트하는 이유가 뭘까요?
또 Mock객체의 동작을 정의한다고하면서 값을 부여할까요?
Q4. 왜 굳이 mock 객체를 사용하는 이유가 뭐야?
mock객체를 사용하면 테스트 대상 유닛의 동작을 해당 의존성에서 분리할 수 있습니다.
이를 통해 종속성의 정확성에 대해 걱정하지 않고 장치의 특정 동작을 테스트하는 데 집중할 수 있습니다.
첫번째 질문에서 Unit테스트는 "격리된 상태(isolation)"에서 함수나 컴포넌트가 올바르게 작동하는지를 확인한다고 하였습니다.
격리되지 않는 상태에서 테스트한다는 말은 "테스트해야할 기능에는 집중을 하지 못하고 있다"는 말을 의미합니다.
테스트 대상인 Calculator클래스가 의존성 분리되지 못한 상태에서 테스트를 한다며
Calculator의 정확한 에러 원인을 찾아내기가 어렵기 때문에 테스트 코드의 의미를 찾기 힘들게 됩니다.
그렇다면 테스트할 대상을 집중하기위해서 어떻게 격리시키면 되는 걸까요?
그 방법으로는 TestDouble을 사용하는 것입니다.
테스트 더블은 실제 의존성의 동작을 모방하지만 테스트 목적으로 특별히 설계된 object입니다.
즉, 의존성이 분리된 객체를 똑같이 하나 더 만들어서 그 녀석으로 테스트를 한다는 의미입니다.
테스트 더블에서 Dummy objects, Stub objects, Fake objects, Mock object같은 개념의 object들이 있습니다.
그중에서 Mockito는 Mock object라는 방식의 테스트 더블을 이용합니다.
그렇다면 Mockito를 사용하여 아래 ViewModel을 Unit테스트 해볼까요?
class MyViewModel : ViewModel() {
private val _userData = MutableLiveData<User>()
val userData: LiveData<User> = _userData
// UserRepository 추가
private val userRepository = UserRepository()
fun updateUser(newUser: User) {
//_userData.value = newUser
viewModelScope.launch {
val newUser = userRepository.getUser(userId)
_userData.value = newUser
}
}
}
Q5. Mockito를 사용하여 이 예제에서 Unit테스트하는 예제를 보여줘
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
class MyViewModelTest {
@get:Rule // (1)
val instantExecutorRule = InstantTaskExecutorRule()
@Mock // (2)
private lateinit var userRepository: UserRepository
@Mock // (3)
private lateinit var observer: Observer<User>
private lateinit var myViewModel: MyViewModel
@Before
fun setup() {
MockitoAnnotations.initMocks(this) //(4)
myViewModel = MyViewModel(userRepository) // (5)
myViewModel.userData.observeForever(observer) // (6)
}
@Test
fun updateUser_success() = runBlocking { // (7) use runBlocking
// Create a mock user
val mockUser = User("John", 30)
// (8) Mock the UserRepository's getUser() method
Mockito.`when`(userRepository.getUser(Mockito.anyString())).thenReturn(mockUser)
// (9) Call the method under test
myViewModel.updateUser(mockUser)
// (10) Verify that the userRepository.getUser() method was called with the correct user ID
Mockito.verify(userRepository).getUser(Mockito.anyString())
// (11) Verify that the LiveData was updated with the correct user
Mockito.verify(observer).onChanged(mockUser)
}
}
우리는 ViewModel에서 만들어놓은 메서드가 잘 작동이되느지 확인을 해야합니다.
updateUser()메서드가 잘 작동이되는지를 판단하려면 아래의 요소를 확인해야합니다.
- userRepository에서 데이터를 잘 가져왔는지?
- observer가 잘 작동중인지?
그렇다면 이 요소를 비교할 수 있게 userRepository인스턴스와 observer인스턴스를 mock객체로 생성하여
실제값과 일치하는지 또는 작동 여부를 통해서 updateUser()메서드의 성공여부를 확인하는 것을 목표로 해야합니다.
UnitTest의 핵심은 어떤 요소들을 테스트해야하는지를 캐치해내는게 가장 중요한 포인트입니다.
(1) instantExecutorRule 테스트 규칙 생성
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
instantExecutorRule은 JUnit 테스트 규칙입니다.
비동기 로직으로 실행되는 LiveData 작업을 테스트 코드안에서 즉시 동기적으로 실행되도록 할 수 있습니다.
이렇게 하여 비동기 로직에서 세팅을 하지 않도록 도와주어 프로세스를 단순화시킵니다.
아래 코드는 instantExecutorRule을 설정하면 사용해야할 코드들입니다.
코루틴으로 비동기 처리를 해줘야하는 과정을 거치게됩니다.
@ExperimentalCoroutinesApi
class MyViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@Mock
private lateinit var userRepository: UserRepository
@Mock
private lateinit var observer: Observer<User>
private lateinit var myViewModel: MyViewModel
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
myViewModel = MyViewModel(userRepository)
myViewModel.userData.observeForever(observer)
}
@Test
fun updateUser_success() = runBlockingTest {
// Create a mock user
val mockUser = User("John", 30)
// Mock the UserRepository's getUser() method
Mockito.`when`(userRepository.getUser(Mockito.anyString())).thenReturn(mockUser)
// Call the method under test
myViewModel.updateUser(mockUser)
// Verify that the userRepository.getUser() method was called with the correct user ID
Mockito.verify(userRepository).getUser(Mockito.anyString())
// Verify that the LiveData was updated with the correct user
Mockito.verify(observer).onChanged(mockUser)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Before
fun setupMainDispatcher() {
Dispatchers.setMain(Dispatchers.Main)
}
// ... Additional tests
}
(2)(3) UserRepository와 observer 클래스를 mock객체로 설정합니다.
(4) 테스트가 실행되기 전 mock객체들을 초기화합니다.
(5)(6) 격리된 상태에서 실제 MyViewModel이 잘 작동되는지를 확인하기위해 MyViewModel 인스턴스 생성하여
종속성이 분리된 mock객체 userRepositoty인스턴스와 observer인스턴스를 넣어줍니다.
(7) 1번과 마찬가지로 테스트할 updateUser_success()함수도 runBlocking을 사용하여
동기 함수인 상태에서 테스트할 수 있게하여 프로세스를 단순화시킵니다.
(8) 이제 mock객체의 동작을 정의하여서 UserRepository에서 가져와야할 값을 임의의 데이터로 세팅합니다.
(9) 그리고 updateUser()메서드를 실행합니다.
(10) 첫번째로 UserRepository에서 getUser() 메서드가 호출되었는지를 확인합니다.
(11) 그 다음 Observer에서 onChaged()메서드가 호출되었는지를 확인합니다.
만약 10번과 11번이 올바르게 작동인 된다면 MyViewModel은 잘 동작한다고 판단할 수 있습니다.
이렇게 마지막에 검증하는 요소도 매우 중요합니다.
Mockito에서 검증을 할 수 있는 메서드를 한번 알아보겠습니다.
Q7. Mockito.verity() 메서드에 대해서 설명해줘
Mockito.verify() 메서드는 Mockito에서 테스트 실행 중 mock객체의 동작을 확인하는 데 사용됩니다.
다음과 같은 내용을 확인할 수 있습니다.
-mock객체의특정 메서드가 호출 여부
-mock객체의 특정 메서드가 호출된 횟수나 인수
아래와 같은 메서드들이 있습니다.
1. Mockito.verify(mockObject).메서드명()
: 해당 메서드가 적어도 한 번은 mockObject에서 호출되었는지 확인합니다.
메서드가 호출되지 않으면 테스트가 실패합니다.
2. Mockito.verify(mockObject, times(n)).메서드명()
: 해당 메서드가 mockObject에서 정확히 n번 호출되었는지 확인합니다.
메서드가 n번 호출되지 않으면 테스트가 실패합니다.
3. Mockito.verify(mockObject, atLeast(n)).메서드명()
: 해당 메서드가 mockObject에서 n번 이상 호출되었는지 확인합니다.
메서드가 n번 이상 호출되지 않으면 테스트가 실패합니다.
4. Mockito.verify(mockObject, atMost(n)).메서드명()
: 지정된 메서드가 mockObject에서 최대 n번 호출되었는지 확인합니다.
메서드가 n번 이상 호출되면 테스트가 실패합니다.
5. Mockito.verify(mockObject, never()).메서드명()
: 지정된 메서드가 mockObject에서 호출되지 않았는지 확인합니다.
메서드가 한 번 이상 호출된 경우 테스트가 실패합니다.
그렇다면 1번 검증 메서드를 사용하여 위의 MyViewModel의 테스트한거였습니다.
더 나아가서 아래처럼 해당메서드에서 특정 인자를 넣은 메서드를 사용한 여부 또한 확인할 수 있습니다.
Mockito.verify(mockObject).메서드명(argument1)
메서드가 비슷한 양식으로 사용되는 것 정도만 알아두고
앞으로 우리는 1번 메서드를 통해서 많은 결과를 검증해야할 것 같습니다.
이를 통해서 viewModel에서 할 수 있는 단위테스트에 대해서 알아보았습니다.
외워야할 내용도, 알아야할 내용도, 상황마다 사용해야할 메서드들도 다양하여서
다소 러닝커브가 높은 편인 것 같습니다.
그렇기 때문에 테스트코드 사용 여부는
초급 개발자의 탈출을 할 때가 되었다는 의미가 아닌가 생각합니다.
그렇다면 여기서 마치겠습니다. 감사합니다.
'안드로이드 연구소 > 테스트코드' 카테고리의 다른 글
TestCode 간단한 예제(feat. Mockito) (0) | 2023.08.14 |
---|---|
[연봉 5000 넘는 안드로이드 개발자 되는 법] Instrumentation테스트란(Espresso사용법) (0) | 2023.05.21 |
[연봉 5000 넘는 안드로이드 개발자 되는 법] 안드로이드 테스트코드 종류 (1) | 2023.05.18 |