Skip to content

Commit

Permalink
Merge pull request #85 from Parsely/functional_tests
Browse files Browse the repository at this point in the history
  • Loading branch information
wzieba authored Oct 30, 2023
2 parents 2bec4fe + e6727a0 commit fcb6d7c
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 6 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,33 @@ jobs:
with:
name: artifact
path: ~/.m2/repository/com/parsely/parsely/*
functional-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Functional Tests
uses: reactivecircus/[email protected]
with:
working-directory: .
api-level: 31
profile: Nexus 6
arch: x86_64
force-avd-creation: false
avd-name: macOS-avd-x86_64-31
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew :parsely:connectedDebugAndroidTest
- name: Publish build artifacts
uses: actions/upload-artifact@v3
if: always()
with:
name: artifact
path: ./parsely/build/reports/*
24 changes: 21 additions & 3 deletions parsely/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ plugins {
id 'org.jetbrains.kotlinx.kover'
}

ext {
assertJVersion = '3.24.2'
mockWebServerVersion = '4.12.0'
}

android {
compileSdkVersion 33

defaultConfig {
minSdkVersion 21
targetSdkVersion 33

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
buildTypes {
release {
Expand Down Expand Up @@ -52,12 +63,19 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'

testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'androidx.test:core:1.5.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
testImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion"

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation "org.assertj:assertj-core:$assertJVersion"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion"
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestUtil 'androidx.test:orchestrator:1.4.2'
}

apply from: "${rootProject.projectDir}/publication.gradle"
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.parsely.parselyandroid

import android.app.Activity
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import java.io.File
import java.io.FileInputStream
import java.io.ObjectInputStream
import java.lang.reflect.Field
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.yield
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith


@RunWith(AndroidJUnit4::class)
class FunctionalTests {

private lateinit var parselyTracker: ParselyTracker
private val server = MockWebServer()
private val url = server.url("/").toString()
private lateinit var appsFiles: Path

private fun beforeEach(activity: Activity) {
appsFiles = Path(activity.filesDir.path)

if (File("$appsFiles/$localStorageFileName").exists()) {
throw RuntimeException("Local storage file exists. Something went wrong with orchestrating the tests.")
}
}

/**
* In this scenario, the consumer application tracks more than 50 events-threshold during a flush interval.
* The SDK will save the events to disk and send them in the next flush interval.
* At the end, when all events are sent, the SDK will delete the content of local storage file.
*/
@Test
fun appTracksEventsAboveQueueSizeLimit() {
ActivityScenario.launch(SampleActivity::class.java).use { scenario ->
scenario.onActivity { activity: Activity ->
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity)

repeat(51) {
parselyTracker.trackPageview("url", null, null, null)
}
}

// Waits for the SDK to send events (flush interval passes)
val requestPayload = server.takeRequest().toMap()
assertThat(requestPayload["events"]).hasSize(51)

// Wait a moment to give SDK time to delete the content of local storage file
waitFor { locallyStoredEvents.isEmpty() }
assertThat(locallyStoredEvents).isEmpty()
}
}

private fun RecordedRequest.toMap(): Map<String, List<Event>> {
val listType: TypeReference<Map<String, List<Event>>> =
object : TypeReference<Map<String, List<Event>>>() {}

return ObjectMapper().readValue(body.readUtf8(), listType)
}

@JsonIgnoreProperties(ignoreUnknown = true)
data class Event(
@JsonProperty("idsite") var idsite: String,
)

private val locallyStoredEvents
get() = FileInputStream(File("$appsFiles/$localStorageFileName")).use {
ObjectInputStream(it).use { objectInputStream ->
@Suppress("UNCHECKED_CAST")
objectInputStream.readObject() as ArrayList<Map<String, Any>>
}
}

private fun initializeTracker(activity: Activity): ParselyTracker {
return ParselyTracker.sharedInstance(
siteId, flushInterval.inWholeSeconds.toInt(), activity.application
).apply {
val f: Field = this::class.java.getDeclaredField("ROOT_URL")
f.isAccessible = true
f.set(this, url)
}
}

private companion object {
const val siteId = "123"
const val localStorageFileName = "parsely-events.ser"
val flushInterval = 10.seconds
}

class SampleActivity : Activity()

private fun waitFor(condition: () -> Boolean) = runBlocking {
withTimeoutOrNull(500.milliseconds) {
while (true) {
yield()
if (condition()) {
break
}
}
}
}
}
11 changes: 11 additions & 0 deletions parsely/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.parsely.parselyandroid">

<uses-permission android:name="android.permission.INTERNET"/>
<application
android:usesCleartextTraffic="true">
<activity android:name=".FunctionalTests$SampleActivity"/>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ public class ParselyTracker {
private static final int QUEUE_SIZE_LIMIT = 50;
private static final int STORAGE_SIZE_LIMIT = 100;
private static final String STORAGE_KEY = "parsely-events.ser";
// emulator localhost
// private static final String ROOT_URL = "http://10.0.2.2:5001/";
private static final String ROOT_URL = "https://p1.parsely.com/";
@SuppressWarnings("StringOperationCanBeSimplified")
// private static final String ROOT_URL = "http://10.0.2.2:5001/".intern(); // emulator localhost
private static final String ROOT_URL = "https://p1.parsely.com/".intern();
protected ArrayList<Map<String, Object>> eventQueue;
private boolean isDebug;
private final Context context;
Expand Down

0 comments on commit fcb6d7c

Please sign in to comment.