Unit Test(Feat. RxJava2, Retrofit, Mokito, MockWebServer)
- 효과적인 단위 테스트 빌드(코드랩 포함)
- 앱 테스트하기
-
Unit Test란 앱 테스트 전략의 가장 기본이 되는 테스트로, 코드에 관한 단위 테스트를 만들고, 실행하여 개별 단위의 로직이 올바른지 확인하는 것이다. 빌드마다 단위 테스트를 실행하여 코드 변경으로 인한 소프트웨어 회귀를 빠르게 파악하고 수정하는 것이 목적이다.
-
안드로이드의 권장 아키텍처를 구현하며 Repository계층의 인증이나 재시도 등등의 기능 추가로 로직이 점점 복잡해졌고, 그에 대한 유닛 테스트의 필요성을 느꼈고 그러던 와중 MockWebServer라는 것을 알게 되었다.
-
HTTP/HTTPS 통신을 사용하는 앱에서 작업을 제대로 처리하고 있는지 테스트 해 볼 수 있는 웹 서버를 제공한다. 기본적인 원리는 로컬 호스트의 특정 포트에 웹 서버를 하나 만들어 놓고, 앞으로 들어올 요청에 대한 Reponse들을 큐에 쌓아 둔 후 요청때마다 하나씩 내보내 주는 것이다. Retrofit객체를 baseUrl만 수정해서 그대로 사용할 수 있어 굳이 Repository가 가진 구현을 테스트용으로 바꾸지 않더라도 테스트 할때 Retrofit만 다른걸로 바꿔 장착해 주면 된다는 장점이 있다. API는 위 링크를 참조. 별게 없다.
-
위를 MockWebServer와 RxJava를 활용한 테스트 코드
- Repository
// 테스트에 적용할 TestRule
class RxSchedulerRule: TestRule {
override fun apply(base: Statement?, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
RxJavaPlugins.setIoSchedulerHandler {
Schedulers.trampoline()
}
RxAndroidPlugins.setInitMainThreadSchedulerHandler {
Schedulers.trampoline()
}
try {
base?.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}
}
@RunWith(MockitoJUnitRunner::class)
class RepositoryTest {
private lateinit var server: MockWebServer
private lateinit var repository: Repository
@get:Rule
val rxSchedulerRule = RxSchedulerRule()
@Before
fun setUp() {
server = MockWebServer()
val api = Api.createTest(server.url("").toString())
repository = Repository(api = api)
}
// 테스트 함수 이름에 'Test'는 안붙이는게 국룰이라고 함
@Test
fun requestAccessToken_response_200() {
val body = ResponseMocks.requestAccessTokenMock
server.enqueue(MockResponse()
.setResponseCode(200)
.setBody(body))
val subscriber = TestObserver<Unit>()
repository.requestAccessToken()
.subscribe(subscriber)
subscriber.assertComplete()
subscriber.assertNoErrors()
}
@Test
fun requestAccessToken_response_400() {
val body = ResponseMocks.emptyMock
server.enqueue(MockResponse()
.setResponseCode(400)
.setBody(body))
val subscriber = TestObserver<Unit>()
repository.requestAccessToken()
.subscribe(subscriber)
subscriber.assertError(HttpException::class.java)
}
@After
fun shutdown() {
server.shutdown()
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
- ViewModel
@RunWith(MockitoJUnitRunner::class)
class MyPageViewModelTest {
private lateinit var server: MockWebServer
private lateinit var viewModel: MyPageViewModel
@get:Rule
val rxSchedulerRule = RxSchedulerRule()
@Before
fun setUp() {
server = MockWebServer()
val api = Api.createTest(server.url("").toString())
val repository = Repository(api = api)
viewModel = MyPageViewModel(repository)
}
@Test
fun requestLogin_200() {
val body = Mocks.requestLogin_200
server.enqueue(MockResponse()
.setResponseCode(200)
.setBody(body))
val observer = TestObserver<LoginResponseModel>()
viewModel.versionState.subscribe(observer)
val param = HashMap<String, Any>()
param["id"] = "aaaaa"
param["password"] = "pppppaaasword"
viewModel.requestLogin(param)
observer.assertNoErrors()
observer.assertValue {
it.accessToken == "accessToken"
}
}
@Test
fun requestLogin_400() {
val body = Mocks.requestLogin_400
server.enqueue(MockResponse()
.setResponseCode(400)
.setBody(body))
val observer = TestObserver<MyPageViewModel.ErrorState>()
viewModel.errorState.subscribe(observer)
val param = HashMap<String, Any>()
param["id"] = "aaaaa"
param["password"] = "pppppaaasword"
viewModel.requestLogin(param)
observer.assertValue {
it is MyPageViewModel.ErrorState.LoginError
&& it.message == "오류가 발생했습니다.\n새로고침 해주세요."
}
}
@After
fun shutdown() {
server.shutdown()
}
}