Last active
December 6, 2019 11:33
-
-
Save Razeeman/9453d78f223c8660f7cc8ca73e9b3638 to your computer and use it in GitHub Desktop.
Util class for unit tests. Creates subject and mocks instances, resets mocks before each test. Applies Rx rule.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Util class for unit tests. | |
* Creates subject and mocks instances, resets mocks before each test. Applies Rx rule. | |
*/ | |
abstract class SubjectDelegateUnitTest<T : Any> : BaseUnitTest() { | |
@PublishedApi internal var mocks = mutableListOf<Any>() | |
lateinit var subject: T | |
override fun setUp() { | |
super.setUp() | |
reset(*mocks.toTypedArray()) | |
} | |
/** | |
* Should be called in test init block or before each test. | |
*/ | |
inline fun <reified T2> prepareForTest() { | |
processConstructor(T2::class.java) | |
processFields(T2::class.java) | |
} | |
inline fun <reified T3> getMock(): T3 { | |
return mocks.find { it is T3 } as? T3 | |
?: throw IllegalArgumentException("Cannot find mock instance of ${T3::class.java.simpleName}") | |
} | |
// Mock constructor parameters | |
@PublishedApi internal fun processConstructor(clazz: Class<*>) { | |
val constructor = clazz.constructors[0] | |
val params = constructor.parameterTypes.map(::processClass) | |
mocks.addAll(params.filter { canBeMocked(it::class.java) }) | |
@Suppress("UNCHECKED_CAST") | |
subject = constructor.newInstance(*params.toTypedArray()) as? T | |
?: throw IllegalArgumentException("Cannot create subject instance") | |
} | |
// Mock injectable fields | |
@PublishedApi internal fun processFields(clazz: Class<*>) { | |
clazz.fields.forEach { field -> | |
processField(field)?.let { param -> | |
if (canBeMocked(param::class.java)) mocks.add(param) | |
field.set(subject, param) | |
} | |
} | |
} | |
private fun processClass(clazz: Class<*>): Any { | |
return if (canBeMocked(clazz)) Mockito.mock(clazz, Mockito.RETURNS_SMART_NULLS) | |
else when (clazz) { | |
// Add more types if necessary | |
String::class.java -> TEST_STRING | |
else -> throw IllegalArgumentException("Cannot mock this class ${clazz.simpleName}") | |
} | |
} | |
private fun processField(field: Field): Any? = | |
if (fieldInjected(field)) processClass(field.type) else null | |
private fun canBeMocked(clazz: Class<*>): Boolean = | |
MockUtil.typeMockabilityOf(clazz).mockable() | |
private fun fieldInjected(field: Field): Boolean = | |
field.annotations.find { it.annotationClass.toString().contains("javax.inject.Inject") } != null | |
companion object { | |
@JvmField @ClassRule | |
val rule = Rules.RxImmediateSchedulerRule() | |
} | |
} | |
abstract class BaseUnitTest { | |
companion object { | |
const val TEST_BOOLEAN = true | |
const val TEST_STRING = "test_string" | |
const val TEST_LONG = 123L | |
const val TEST_INT = 45 | |
const val TEST_FLOAT = 6.7f | |
const val TEST_DOUBLE = 8.9 | |
const val TEST_EMPTY_STRING = "" | |
const val TEST_ID = "test_id" | |
const val TEST_LOGIN = "test_login" | |
const val TEST_PASSWORD = "test_password" | |
const val TEST_CODE = "test_code" | |
const val TEST_USER_TOKEN = "user_token" | |
const val TEST_PHONE = "test_phone" | |
const val TEST_URL = "test_url" | |
const val TEST_EMAIL = "test_email" | |
} | |
@Before | |
open fun setUp() {} | |
@After | |
open fun tearDown() {} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment