diff --git a/FEATURES.md b/FEATURES.md index 237db95..89da679 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -87,3 +87,11 @@ Base implementation of a `DaggerFragment` that is MVP-ready (you can access the It also provides this extra feature: - `requireArgument(key: String): T`: get argument by the given [key] and returns it as a non-null [T]. + +# Wolmo testing features + +### WolmoPresenterTest +A base that setups the environment for a Wolmo's [BasePresenter] test. It also provides a prepared environment for annotated mocks. + +### CoroutineTestRule +A Junit Test Rule that allows to use Coroutines main dispatcher on a test. If [runOnAllTests] is false then all tests will have this configuration, otherwise just those that have [CoroutineTest] annotation. diff --git a/core/build.gradle b/core/build.gradle index 9dc988e..8931283 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -82,7 +82,7 @@ dependencies { // Coroutines api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" // Test testImplementation "junit:junit:$junit_version" diff --git a/core/src/main/java/ar/com/wolox/wolmo/core/tests/CoroutineTestRule.kt b/core/src/main/java/ar/com/wolox/wolmo/core/tests/CoroutineTestRule.kt new file mode 100644 index 0000000..3b31b68 --- /dev/null +++ b/core/src/main/java/ar/com/wolox/wolmo/core/tests/CoroutineTestRule.kt @@ -0,0 +1,48 @@ +package ar.com.wolox.wolmo.core.tests + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import java.util.concurrent.Executors + +/** + * If a test method is annotated with this, then the [CoroutineTestRule] will be executed. + * Use it when [CoroutineTestRule.runOnAllTests] is false. + */ +annotation class CoroutineTest + +/** + * A Junit Test Rule that allows to use Coroutines main dispatcher on a test. + * If [runOnAllTests] is false then all tests will have this configuration, + * otherwise just those that have [CoroutineTest] annotation. + */ +@ExperimentalCoroutinesApi +class CoroutineTestRule(private val runOnAllTests: Boolean = false) : TestWatcher() { + + private val mainThreadSurrogate = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + private fun isCoroutineTest(description: Description?): Boolean { + return description?.annotations?.filterIsInstance()?.isNotEmpty() == true + } + + private fun shouldRunRule(description: Description?): Boolean { + return runOnAllTests || isCoroutineTest(description) + } + + override fun starting(description: Description?) { + if (shouldRunRule(description)) { + Dispatchers.setMain(mainThreadSurrogate) + } + } + + override fun finished(description: Description?) { + if (shouldRunRule(description)) { + Dispatchers.resetMain() + mainThreadSurrogate.close() + } + } +} diff --git a/core/src/main/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTest.kt b/core/src/main/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTest.kt index 88ebca9..633f0ad 100644 --- a/core/src/main/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTest.kt +++ b/core/src/main/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTest.kt @@ -1,47 +1,41 @@ package ar.com.wolox.wolmo.core.tests import ar.com.wolox.wolmo.core.presenter.BasePresenter +import org.junit.After import org.junit.Before import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import java.lang.reflect.ParameterizedType /** * A base that setups the environment for a Wolmo's [BasePresenter] test. + * It also provides a prepared environment for annotated mocks. */ abstract class WolmoPresenterTest> { - /** - * The presenter to be tested. - */ + /** The presenter to be tested */ protected lateinit var presenter: P - /** - * The mocked view that the presenter will use. - */ - lateinit var view: V + /** The mocked view attached to the presenter. */ + protected lateinit var view: V @Suppress("UNCHECKED_CAST") private fun getViewClass(): Class { return (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class } - /** - * Setup the environment. - */ + /** Setup the environment. */ @Before fun setupWolmoPresenterTest() { MockitoAnnotations.initMocks(this) presenter = getPresenterInstance() - view = Mockito.mock(getViewClass()) - presenter.attachView(view) + view = mock(getViewClass()).also { presenter.attachView(it) } } /** * This method provides the presenter instance that will be tested. * If the presenter should be spied, it's possible to return the presenter spied with [Mockito.spy]. - * - * @return the [BasePresenter] instance. */ abstract fun getPresenterInstance(): P } \ No newline at end of file diff --git a/core/src/main/java/ar/com/wolox/wolmo/core/util/NavigationUtils.kt b/core/src/main/java/ar/com/wolox/wolmo/core/util/NavigationUtils.kt index e3368c3..ef78b4d 100644 --- a/core/src/main/java/ar/com/wolox/wolmo/core/util/NavigationUtils.kt +++ b/core/src/main/java/ar/com/wolox/wolmo/core/util/NavigationUtils.kt @@ -81,17 +81,14 @@ fun Context.makeCall(phone: String) { } /** - * Sends an intent to start an [Activity] for the provided [clazz] from a [context] + * Sends an intent to start an [Activity] for the provided [clazz] from a [Context] * with a variable number of instances of [intentExtras] that will be sent as extras. */ @SafeVarargs -fun Context.jumpTo( - clazz: Class<*>, - vararg intentExtras: IntentExtra -) = jumpTo(clazz, null, *intentExtras) +fun Context.jumpTo(clazz: Class<*>, vararg intentExtras: IntentExtra) = jumpTo(clazz, null, *intentExtras) /** - * Sends an intent to start an [Activity] for the provided [clazz] from a [context] + * Sends an intent to start an [Activity] for the provided [clazz] from a [Context] * with a variable number of instances of [intentExtras] that will be sent as extras. * It accepts a [transition] that defines the animation behaviour. */ diff --git a/core/src/test/java/ar/com/wolox/wolmo/core/tests/CoroutineTestRuleTest.kt b/core/src/test/java/ar/com/wolox/wolmo/core/tests/CoroutineTestRuleTest.kt new file mode 100644 index 0000000..7afb8c2 --- /dev/null +++ b/core/src/test/java/ar/com/wolox/wolmo/core/tests/CoroutineTestRuleTest.kt @@ -0,0 +1,37 @@ +package ar.com.wolox.wolmo.core.tests + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.Rule +import org.junit.Test + +@ExperimentalCoroutinesApi +class CoroutineTestRuleTest { + + @get:Rule + val coroutineTestRule = CoroutineTestRule(runOnAllTests = true) + + @Test(expected = Test.None::class /* no exception expected */) + fun `given a rule with run on all tests true when run coroutine on main then there's no exception thrown`() { + runBlocking(Dispatchers.Main) {} + } +} + +@ExperimentalCoroutinesApi +class CoroutineTestRuleTest2 { + + @get:Rule + val coroutineTestRule = CoroutineTestRule(runOnAllTests = false) + + @Test(expected = IllegalStateException::class) + fun `given a rule with run on all tests false when run coroutine on main then a IllegalStateException is thrown`() { + runBlocking(Dispatchers.Main) {} + } + + @Test(expected = Test.None::class /* no exception expected */) + @CoroutineTest + fun `given a rule with run on all tests false and annotated with CoroutineScope when run coroutine on main then there's no exception thrown`() { + runBlocking(Dispatchers.Main) {} + } +} \ No newline at end of file diff --git a/core/src/test/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTestTest.kt b/core/src/test/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTestTest.kt new file mode 100644 index 0000000..4d08374 --- /dev/null +++ b/core/src/test/java/ar/com/wolox/wolmo/core/tests/WolmoPresenterTestTest.kt @@ -0,0 +1,57 @@ +package ar.com.wolox.wolmo.core.tests + +import android.graphics.Rect +import ar.com.wolox.wolmo.core.presenter.BasePresenter +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +class WolmoPresenterTestTest : WolmoPresenterTest() { + + @Mock + lateinit var rectMocked: Rect + + override fun getPresenterInstance() = TestPresenter() + + private fun tryToGet(getter: () -> T): T? = try { + getter() + } catch (ignored: Exception) { + null + } + + @Test + fun `given a test when getting instances then those are not null`() { + + val presenter = tryToGet { presenter } + val view = tryToGet { view } + + assertThat(presenter, `is`(notNullValue())) + assertThat(view, `is`(notNullValue())) + } + + @Test + fun `given an annotated mock when using it then it's not null`() { + + val rectMocked = tryToGet { rectMocked } + + assertThat(rectMocked, `is`(notNullValue())) + } + + @Test + fun `given a presenter and a view when getting presenter get a call view request then it calls the view`() { + presenter.onCallViewRequest() + verify(view, times(1)).doSomething() + } + + interface TestView { + fun doSomething() + } + + class TestPresenter : BasePresenter() { + fun onCallViewRequest() = view?.doSomething() + } +} \ No newline at end of file