Skip to content
This repository has been archived by the owner on Sep 13, 2023. It is now read-only.

Commit

Permalink
add tests to support the events implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
vahidlazio committed Jul 21, 2023
1 parent bcc6149 commit 2dc402e
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 18 deletions.
7 changes: 4 additions & 3 deletions OpenFeature/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ android {
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2")
}

afterEvaluate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ fun OpenFeatureClient.toAsync(): AsyncClient {
return AsyncClientImpl(this)
}

fun observeProviderReady() = observeProviderEvents()
fun observeProviderReady() = EventHandler.eventsObserver()
.observe<OpenFeatureEvents.ProviderReady>()
.onStart {
if (EventHandler.providerStatus().isProviderReady()) {
this.emit(OpenFeatureEvents.ProviderReady)
}
}

fun observeProviderEvents() = EventHandler.eventsObserver()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.openfeature.sdk.events

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
Expand All @@ -24,10 +25,10 @@ interface EventsPublisher {

inline fun <reified T : OpenFeatureEvents> EventObserver.observe() = observe(T::class)

class EventHandler : EventObserver, EventsPublisher, ProviderStatus {
class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPublisher, ProviderStatus {
private val sharedFlow: MutableSharedFlow<OpenFeatureEvents> = MutableSharedFlow()
private val isProviderReady = MutableStateFlow(false)
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val coroutineScope = CoroutineScope(dispatcher)

init {
coroutineScope.launch {
Expand All @@ -52,26 +53,29 @@ class EventHandler : EventObserver, EventsPublisher, ProviderStatus {
}
}

override fun <T : OpenFeatureEvents> observe(kClass: KClass<T>): Flow<T> = sharedFlow
.filterIsInstance(kClass)

override fun isProviderReady(): Boolean {
return isProviderReady.value
}

override fun <T : OpenFeatureEvents> observe(kClass: KClass<T>): Flow<T> = sharedFlow
.filterIsInstance(kClass)

companion object {
@Volatile
private var instance: EventHandler? = null

private fun getInstance() =
private fun getInstance(dispatcher: CoroutineDispatcher) =
instance ?: synchronized(this) {
instance ?: create().also { instance = it }
instance ?: create(dispatcher).also { instance = it }
}

fun eventsObserver(): EventObserver = getInstance()
fun providerStatus(): ProviderStatus = getInstance()
fun eventsPublisher(): EventsPublisher = getInstance()
fun eventsObserver(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventObserver =
getInstance(dispatcher)
fun providerStatus(dispatcher: CoroutineDispatcher = Dispatchers.IO): ProviderStatus =
getInstance(dispatcher)
fun eventsPublisher(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventsPublisher =
getInstance(dispatcher)

private fun create() = EventHandler()
private fun create(dispatcher: CoroutineDispatcher) = EventHandler(dispatcher)
}
}
209 changes: 209 additions & 0 deletions OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package dev.openfeature.sdk

import dev.openfeature.sdk.async.observeProviderReady
import dev.openfeature.sdk.async.toAsync
import dev.openfeature.sdk.events.EventHandler
import dev.openfeature.sdk.events.OpenFeatureEvents
import dev.openfeature.sdk.events.observe
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
import org.mockito.Mockito.`when`
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import kotlin.time.Duration.Companion.milliseconds

@OptIn(ExperimentalCoroutinesApi::class)
class EventsHandlerTest {

@Test
fun observing_event_observer_works() = runTest {
val eventObserver = EventHandler.eventsObserver()
val eventPublisher = EventHandler.eventsPublisher()
var emitted = false

val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
eventObserver.observe<OpenFeatureEvents.ProviderReady>()
.take(1)
.collect {
emitted = true
}
}

eventPublisher.publish(OpenFeatureEvents.ProviderReady)
job.join()
Assert.assertTrue(emitted)
}

@Test
fun multiple_subscribers_works() = runTest {
val eventObserver = EventHandler.eventsObserver()
val eventPublisher = EventHandler.eventsPublisher()
val numberOfSubscribers = 10
val parentJob = Job()
var emitted = 0

repeat(numberOfSubscribers) {
CoroutineScope(parentJob).launch(UnconfinedTestDispatcher(testScheduler)) {
eventObserver.observe<OpenFeatureEvents.ProviderReady>()
.take(1)
.collect {
emitted += 1
}
}
}

eventPublisher.publish(OpenFeatureEvents.ProviderReady)
parentJob.children.forEach { it.join() }
Assert.assertTrue(emitted == 10)
}

@Test
fun canceling_one_subscriber_does_not_cancel_others() = runTest {
val eventObserver = EventHandler.eventsObserver()
val eventPublisher = EventHandler.eventsPublisher()
val numberOfSubscribers = 10
val parentJob = Job()
var emitted = 0

val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
eventObserver.observe<OpenFeatureEvents.ProviderReady>()
.take(1)
.collect {}
}

repeat(numberOfSubscribers) {
CoroutineScope(parentJob).launch(UnconfinedTestDispatcher(testScheduler)) {
eventObserver.observe<OpenFeatureEvents.ProviderReady>()
.take(1)
.collect {
emitted += 1
}
}
}
job.cancel()
eventPublisher.publish(OpenFeatureEvents.ProviderReady)
parentJob.children.forEach { it.join() }
Assert.assertTrue(emitted == 10)
}

@Test
fun the_provider_status_stream_works() = runTest {
val eventPublisher = EventHandler.eventsPublisher()
var isProviderReady = false

// observing the provider status after the provider ready event is published
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
observeProviderReady()
.take(1)
.collect {
isProviderReady = true
}
}

eventPublisher.publish(OpenFeatureEvents.ProviderReady)
job.join()
Assert.assertTrue(isProviderReady)
}

@Test
fun the_provider_status_stream_not_emitting_without_event_published() = runTest {
var isProviderReady = false

// observing the provider status after the provider ready event is published
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
observeProviderReady()
.timeout(10L.milliseconds)
.collect {
isProviderReady = true
}
}

job.join()
Assert.assertTrue(!isProviderReady)
}

@Test
fun the_provider_status_stream_is_replays_current_status() = runTest {
val eventPublisher = EventHandler.eventsPublisher()
eventPublisher.publish(OpenFeatureEvents.ProviderReady)
var isProviderReady = false

// observing the provider status after the provider ready event is published
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
observeProviderReady()
.take(1)
.collect {
isProviderReady = true
}
}

job.join()
Assert.assertTrue(isProviderReady)
}

@Test
fun observe_string_value_from_client_works() = runTest {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
val eventPublisher = EventHandler.eventsPublisher(testDispatcher)
eventPublisher.publish(OpenFeatureEvents.ProviderReady)
val key = "mykey"
val default = "default"
val resultTexts = mutableListOf<String>()

val mockOpenFeatureClient = mock<OpenFeatureClient> {
on { getStringValue(key, default) } doReturn "text1"
}

// observing the provider status after the provider ready event is published
val job = backgroundScope.launch(testDispatcher) {
mockOpenFeatureClient.toAsync()
.observeStringValue(key, default)
.take(2)
.collect {
resultTexts.add(it)
}
}

`when`(mockOpenFeatureClient.getStringValue(key, default))
.thenReturn("text2")

eventPublisher.publish(OpenFeatureEvents.ProviderReady)
job.join()
Assert.assertEquals(listOf("text1", "text2"), resultTexts)
}

@Test
fun observe_string_value_from_client_waits_until_provider_ready() = runTest {
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
val eventPublisher = EventHandler.eventsPublisher(testDispatcher)
val key = "mykey"
val default = "default"
val resultTexts = mutableListOf<String>()

val mockOpenFeatureClient = mock<OpenFeatureClient> {
on { getStringValue(key, default) } doReturn "text1"
}

// observing the provider status after the provider ready event is published
val job = backgroundScope.launch(testDispatcher) {
mockOpenFeatureClient.toAsync()
.observeStringValue(key, default)
.take(1)
.collect {
resultTexts.add(it)
}
}

eventPublisher.publish(OpenFeatureEvents.ProviderReady)
job.join()
Assert.assertEquals(listOf("text1"), resultTexts)
}
}

0 comments on commit 2dc402e

Please sign in to comment.