모든 개발자들의 숙명
여전히 테스터블한 코드 하하 (지도 못 짬)
그래서 대충 긁어와봤다
1. 인터페이스를 사용할것
인터페이스란? 구현 요구 사항을 위한 빈 껍데기.
캡슐화할때 습관적으로 써야한다~~~~ 좋은 테스트 환경을 만들기 위해서는 구현 클래스가 인터페이스를 받아 구현함으로써 규칙에 맞게 개발되도록 해야한다
// interface
interface LoginRepository {
fun signIn(authId: String): String
}
// concrete implementation class
class LoginRepositoryImpl: LoginRepository {
overide fun signIn(authId: String): String {
// implementation logic
}
}
2. 프레임워크와 라이브러리의 액세스를 방지하기
테스트 가능한 코드는 프레임워크 / 라이브러리의 직접 액세스를 피하는것이 좋다. 물론 항상 이렇게 만들 수는 없겠지만, 테스트시 발생하는 이슈의 로직을 정확히 파악하고 수정하기 위해서는 가급적 피하는것이 좋다.
여기서 포인트는 직접액세스를 피한다는 것인데,
직접 액세스를 피하면서도, 프레임워크와 라이브러리의 기능이 포함된 부분을 테스트 할때는, 아래와 같이 Factory 패턴을 이용하면 좋다.
// SearchClient
interface SearchClient {
fun getResults(query: String): List<String>
}
// SearchClient requires activity or fragment level context because
// client dispose the results when activity or fragment destroy.
class SearchClientImp(private val context: Context): SearchClient {
overide fun getResults(query: String): List<String> {
// return results
}
}
// Repository
interface SearchRepository {
fun search(client: SearchClient, query: String): List<String>
}
class SearchRepositoryImpl: SearchRepository {
overide fun search(client: SearchClient, query: String): List<String> {
return client.getResults(query = query)
}
}
// Factory
interface SearchClientFactory {
fun create(context: Context): SearchClient
}
//Factory Implementation
class SearchClientFactoryImpl: SearchClientFactory {
overide fun create(context: Context): SearchClient {
return SearchClientImpl()
}
}
//ViewModel or Presenter
class SearchViewModel(private val respository: SearchRepository): ViewModel() {
// client must be init from UI because internal search client is attached to context.
private fun search(client: SearchClient) {
repository.search(client, query)
}
}
- 해당 코드에서는 factory를 통해 searchClient 객체를 생성한다. 동시에, 내부 search sdk를 밖으로 노출하지 않는다.
- 컨텍스트나 프레임워크 수준 액세스가 노출되지 않도록 하기 위해 함수 search에서
SearchClient
에 대한 인수를 사용하도록 함.searchSearchViewModel
을 테스트할때, sdk의 컨텍스트 없이 searchClient 의 목데이터로 테스트 가능
DI를 사용할것
interface LoginRepository {
fun signIn(authId: String): String
}
// Concrete Implementation
class LoginRepositoryImpl(
private val networkSource: LoginNetworkDataSource
): LoginRepository {
overide fun signIn(authId: String): String {
//networkSource.signIn(...)
}
}
// ViewModel
class LoginViewModel(private val repository: LoginRepository): ViewModel() {
fun signIn() {
//repository.signIn(....)
}
}
//DI Module
object DependencyGraph {
val viewModel = LoginViewModel(repository = ...)
}
//Testing
class LoginViewModelTest {
@Mock
private val repository = mock<LoginRepository>()
private val sut = LoginViewModel(repository = repository)
@Test
fun given_valid_auth_id_when_sign_in_then_returns_success() {
//....
}
}
- DI를 사용해 의존성을 주입받도록 하면, 블럭처럼 조립해서 테스트 클래스를 만드는것이 쉬워짐
4. 기능을 작게 만들것
테스트 가능한 코드는 바로바로…작은 함수!
항상 SRP를 지켜서 만들것
class LoginRepositoryImpl(
private val network: LoginNetworkSource
): LoginRepository {
overide fun signIn(authId: String): String {
return signIn(network = network, authId = authId)
}
Companion object {
private fun signIn(network: LoginNetworkSource, authId: String): String {
val result = network.signIn(authId)
if (result.isSuccess()) {
submitAuthToken(result.token)
submitAuthSession(result.authId)
} else {
invalidateAuthSession(authId)
}
}
private fun submitAuthToken(token: String) {
//logic...
}
private fun submitAuthSession(authId: String) {
//logic...
}
private fun invalidateAuthSession(authId: String) {
//logic...
}
}
}
Uploaded by N2T