How to get Worker to run synchronously in Android Instrumentation test? - android-testing

I am running instrumentation tests on a worker WorkerGetData which fetches data from repository.
My test is failing because the worker is running asynchronously on a default dispatcher. The test fails on assertion with expected: SUCCEEDED but was : RUNNING.
How can I get the worker to run synchronously in the test?
#RunWith(AndroidJUnit4::class)
#HiltAndroidTest
class TestClass {
#get:Rule
val hiltRule = HiltAndroidRule(this)
#Inject
lateinit var workerConfiguration: Configuration
#Before
fun setup() {
hiltRule.inject()
context = InstrumentationRegistry.getInstrumentation().targetContext
val config = workerConfiguration
// Initialize WorkManager for instrumentation tests.
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
#Test
fun testGetDataWorker() {
val request = OneTimeWorkRequestBuilder<WorkerGetData>()
.build()
val workManager = WorkManager.getInstance(context)
workManager.enqueue(request).result.get()
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state).isEqualTo(WorkInfo.State.SUCCEEDED)
}
}
#Module
#TestInstallIn(
components = [SingletonComponent::class],
replaces = [WorkManagerModule::class]
)
object FakeWorkManagerModule {
#Singleton
#Provides
fun provideWorkManagerConfiguration(
myDelegatingWorkerFactory: MyDelegatingWorkerFactory,
): Configuration {
return Configuration.Builder()
.setExecutor(SynchronousExecutor())
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.setWorkerFactory(myDelegatingWorkerFactory)
.build()
}
}

Related

Xamarin Android IntentService calling OnDestroy while performing large async operation

Im trying to perform a synchronization task without blocking UI thread. I have implemented a Android Service to do so, but I found out, if the synchronization task needs too much computational time, the UI thread was blocked. So I tried the migration to IntentService. This is how my IntentService looks like:
[Service]
public class SynchronizeIntentService : IntentService
{
static readonly string TAG = typeof(SynchronizeIntentService).FullName;
private NotificationCompat.Builder Builder;
private NotificationManagerCompat NotificationManager;
public SynchronizeIntentService() : base("SynchronizeIntentService")
{
}
public override void OnDestroy()
{
var tmp = 5;
base.OnDestroy();
}
private NotificationChannel createNotificationChannel()
{
var channelId = Constants.NOTIFICATION_CHANNELID;
var channelName = "My Notification Service";
var Channel = new NotificationChannel(channelId, channelName, Android.App.NotificationImportance.Default);
Channel.LightColor = Android.Resource.Color.HoloBlueBright;
Channel.LockscreenVisibility = NotificationVisibility.Public;
return Channel;
}
private void createForegroundService()
{
var mNotificationManager = GetSystemService(Context.NotificationService) as NotificationManager;
if (Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
{
mNotificationManager.CreateNotificationChannel(createNotificationChannel());
}
var notificationBuilder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNELID);
GenerateNotification();
StartForeground(Constants.SERVICE_RUNNING_NOTIFICATION_ID, Builder.Notification);
}
private void GenerateNotification()
{
NotificationManager = NotificationManagerCompat.From(this);
Builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNELID);
Builder.SetContentTitle(ContaScan.Classes.Localize.GetString("Global_SynchProcess", ""))
.SetSmallIcon(Resource.Drawable.icon)
.SetPriority(NotificationCompat.PriorityLow);
}
protected async override void OnHandleIntent(Intent intent)
{
Log.Debug(TAG, "Service Started!");
await Synch();
Log.Debug(TAG, "Service Stopping!");
StopForeground(true);
this.StopSelf();
}
public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId)
{
base.OnStartCommand(intent, flags, startId);
createForegroundService();
return StartCommandResult.Sticky;
}
private async Task Synch()
{
//Large synch process
}
}
And this is how the service is getting started:
startServiceIntent = new Intent(Android.App.Application.Context, typeof(SynchronizeIntentService));
startServiceIntent.SetAction(Constants.ACTION_START_SERVICE);
ContextWrapper contextWrapper = new ContextWrapper(Android.App.Application.Context);
contextWrapper.StartService(startServiceIntent);
The problem is OnDestroy() method is called while the Synch() task is being performed and looks like the IntentService is being killed before ending the process.
What am I doing wrong?
First, check your API level. This class was deprecated in API level 30.
And then, when you use the OnHandleIntent, do not call Service.stopSelf().
This method is invoked on the worker thread with a request to process. Only one Intent is processed at a time, but the processing happens on a worker thread that runs independently from other application logic. So, if this code takes a long time, it will hold up other requests to the same IntentService, but it will not hold up anything else. When all requests have been handled, the IntentService stops itself, so you should not call Service.stopSelf().
For more details, please check the link below. https://developer.android.com/reference/android/app/IntentService#onHandleIntent(android.content.Intent)

Spock is using real methods even though object is mocked

In my app I use spock in version 2.0-M1-groovy-2.5. I have a problem that even though I mokced a class I get npe from the internals of mocked method.
I have simple class called ReactiveRestHighLevelClient which looks like this
open class ReactiveRestHighLevelClient(val restHighLevelClient: RestHighLevelClient, private val objectMapper: ObjectMapper) {
...
fun index(indexRequest: IndexRequest): Mono<IndexResponse> {
return Mono.create<IndexResponse> { sink ->
restHighLevelClient.indexAsync( // < -- HERE I GET THE NPE EVEN THOUGH THE METHOD IS MOCKED
indexRequest,
object : ActionListener<IndexResponse> {
override fun onFailure(e: Exception) {
e.printStackTrace()
sink.error(e)
}
override fun onResponse(response: IndexResponse) {
sink.success(response)
}
}
)
}
}
...
}
I have class ThreadModelElasticsearchService which uses the ReactiveRestHighLevelClient and looks like this:
#Component
class ThreadModelElasticsearchService(
private val objectMapper: ObjectMapper,
private val reactiveElasticsearchClient: ReactiveRestHighLevelClient,
private val extraFactsService: ExtraFactsService,
private val customerDataService: CustomerDataService,
rateLimiterRegistry: RateLimiterRegistry
) {
...
fun save(operationId: String, threadModel: ThreadModel): Mono<ThreadModel> {
val id = threadModel.threadId
?: ThreadModel.createKey(threadModel.partnerId, threadModel.customerId)
return reactiveElasticsearchClient
.index(
IndexRequest(ThreadModel.INDEX, ThreadModel.TYPE, id)
.source(objectMapper.writeValueAsString(threadModel), XContentType.JSON)
)
.thenReturn(threadModel)
.doOnNext { logger.info("[operation_id=$operationId] -- Saved : $it") }
.onErrorMap { e ->
logger.error("[operation_id=$operationId] -- Error calling elasticSearch", e)
ElasticRepositoryError(e)
}
.rateLimit(elasticRateLimiter)
}
...
}
Finally I have my test class called ThreadModelElasticsearchServiceTest which looks like this:
class ThreadModelElasticsearchServiceTest extends Specification {
ReactiveRestHighLevelClient reactiveElasticsearchClient = Mock()
... other mocks
ThreadModelElasticsearchService threadModelReactiveElasticsearchClient = new
ThreadModelElasticsearchService(objectMapper, reactiveElasticsearchClient, extraFactsService, customerDataService, RateLimiterRegistry.of(RateLimiterConfig.ofDefaults()))
def "should work"() {
given:
ThreadModel threadModel = new ThreadModel(...)
reactiveElasticsearchClient.index(_ as IndexRequest) >> Mono.just(indexResponse)
expect:
threadModelReactiveElasticsearchClient.save("should-work", threadModel).block()
}
When I run the tests I get execption like
java.lang.NullPointerException: null
at com.cb.elastic.search.api.elasticsearch.ReactiveRestHighLevelClient$index$1.accept(ReactiveRestHighLevelClient.kt:76)
which points to the body of the index method of the ReactiveRestHighLevelClient mock which is strange.
How to solve this ?

Access to database row in another thread during integration test

I have a problem in a Grails 3.3.8 application after the upgrade from Grails 2.5.6.
I have a service which using Row.findAll() to get records from an H2 database. Then it creates a list of closures for future execution. Then the list is running by a ThreadExecutor via invokeAll(). In each closure I get data via Row.findById().
It is working when I run program, but it does not work in integration tests. I checked that Row.findAll().size() returns 0 inside the closure but just before the invokeAll() it returns a positive number.
Update:
I prepare small test for that:
#Integration
#Rollback
class TestSpec extends Specification {
void "test something"() {
when:
f()
then:
g()
}
private void f() {
Raw raw = new Raw()
raw.text = "text"
raw.save(flush: true)
}
private void g() {
Closure closure = {
try {
def x = rawService.getRawSize()
if (x != 1) throw new Exception("A: x = " + x)
} catch (Exception e) {
e.printStackTrace()
throw e
}
}
def x = rawService.getRawSize()
executorService.invokeAll([closure])
}
}
The code above is not working. It throws an Exception.
Wrap the body of your f() method in a Raw.withNewSession block. This will save your new instance in a separate session. After the closure is finished the session will close, persisting your instance and allowing it to be accessed in another session or thread.
private void f() {
Raw.withNewSession {
Raw raw = new Raw()
raw.text = "text"
raw.save(flush: true)
}
}

Kotlin and Dagger 2: Issues using Provider class

After scouring the internet, there doesn't seem to be a similar issue and it is eating me. In the process of learning Dependency Injection using Dagger 2, I am trying to translate an example from Java to Kotlin. The project compiles fine in Java, but using Kotlin, Does not like the javax.inject.Provider class and fails to build.
What is missing? Is the use of the Provider class incorrect for Kotlin here?
Here is the error from the Gradle event log:
repositorytrends\custom_implementations\RepoTrendsAppComponent.java:8: error: java.util.Map<java.lang.Class<? extends android.app.Activity>,? extends javax.inject.Provider<dagger.android.AndroidInjector.Factory<? extends android.app.Activity>>> cannot be provided without an #Provides-annotated method.
Here is the offending file. The parameter (Map) for the internal constructor is the deciding factor in a successful build:
class ActivityInjector
#Inject internal constructor(private val activityInjectors: Map<Class<out Activity>, Provider<AndroidInjector.Factory<out Activity>>>){
private val cache = HashMap<String, AndroidInjector<out Activity>>()
internal fun inject(activity: Activity) {
if (activity !is RepoTrendActivity) {
throw IllegalArgumentException("Activity must extend BaseActivity")
}
val instanceId = activity.getInstanceID
if (cache.containsKey(instanceId)) {
(cache[instanceId] as AndroidInjector<Activity>).inject(activity)
return
}
val injectorFactory = activityInjectors[activity.javaClass]?.get() as AndroidInjector.Factory<Activity>
val injector = injectorFactory.create(activity)
cache.put(instanceId, injector)
injector.inject(activity)
}
internal fun clear(activity: Activity) {
if (activity !is RepoTrendActivity) {
throw IllegalArgumentException("Activity must extend BaseActivity")
}
cache.remove(activity.getInstanceID)
}
companion object {
internal operator fun get(context: Context): ActivityInjector{
return (context.applicationContext as RepoTrendsApp).activityInjector
}
}
}
Here are the rest of the classes that are related to the Gradle build error log:
#Singleton
#Component(modules = arrayOf(
RepoTrendsAppModule::class
))
interface RepoTrendsAppComponent {
fun inject(repoTrendsApp: RepoTrendsApp)
}
Custom Application file:
class RepoTrendsApp: Application(){
#Inject lateinit var activityInjector: ActivityInjector
}
Build.gradle for good measure:
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 26
defaultConfig {
applicationId 'com.inviscidlabs.repositorytrends'
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
productFlavors {
}
kapt {
generateStubs = true
}
}
dependencies {
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "com.android.support:appcompat-v7:$supportLibraryVersion"
implementation "com.android.support:design:$supportLibraryVersion"
implementation "com.google.dagger:dagger:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
implementation "com.squareup.moshi:moshi:$moshiVersion"
kapt "com.ryanharter.auto.value:auto-value-moshi:$autoValueMoshiVersion"
compileOnly "com.ryanharter.auto.value:auto-value-moshi-annotations:$autoValueMoshiVersion"
compileOnly "com.google.auto.value:auto-value:$autoValueVersion"
annotationProcessor "com.google.auto.value:auto-value:$autoValueVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "com.jakewharton.rxrelay2:rxrelay:$rxRelayVersion"
//Drop in replacement for Fragments
implementation "com.bluelinelabs:conductor:$conductorVersion"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}
As requested, RepoTrendsAppModule:
import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
#Module
class RepoTrendsAppModule(val application: Application){
#Provides
fun provideApplicationContext(): Context = application
}
You don't need your ActivityInjector class. Modify your application class as follow:
class RepoTrendsApp: Application(), HasActivityInjector {
#Inject
internal lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Activity>
override fun onCreate() {
super.onCreate()
DaggerRepoTrendsAppComponent.builder()
.repoTrendsAppModule(RepoTrendsAppModule(this))
.build()
.inject(this)
}
override fun activityInjector(): AndroidInjector<Activity>? {
return dispatchingActivityInjector
}
}
and your component to:
#Singleton
#Component(modules = arrayOf(
AndroidSupportInjectionModule::class,
RepoTrendsAppModule::class
))
interface RepoTrendsAppComponent : AndroidInjector<RepoTrendsApp>
Sounds like some the #Provides annotation is missing on your module. Here's an example Kotlin/Android/Dagger 2 module definition:
#Module
class MyAndroidModule(private val application: Application) {
#Provides
#Singleton
#CustomInjectionAnnotation
fun provideApplicationContext(): Context = application
#Provides
#Singleton
fun provideLocationManager(): LocationManager =
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
#Provides
#Singleton
#Named("version")
fun provideVersionString(): String = "beta"
}

How to use a custom Coder in a PCollection<KV<String, B>>?

I'm trying to use a custom Coder so that I can do some transforms, but I'm having trouble getting the PCollection to use my custom coder, and I suspect (???) it's because it's wrapped in a KV. Specifically:
Pipeline p = Pipeline.create ...
p.getCoderRegistry().registerCoder(MyClass.class, MyClassCoder.class);
...
PCollection<String> input = ...
PCollection<KV<String, MyClass>> t = input.apply(new ToKVTransform());
When I try to run something like this, I get a java.lang.ClassCastException and a stacktrace that includes a SerializableCoder instead of MyClassCoder like I would expect.
[error] at com.google.cloud.dataflow.sdk.coders.SerializableCoder.decode(SerializableCoder.java:133)
[error] at com.google.cloud.dataflow.sdk.coders.SerializableCoder.decode(SerializableCoder.java:50)
[error] at com.google.cloud.dataflow.sdk.coders.KvCoder.decode(KvCoder.java:95)
[error] at com.google.cloud.dataflow.sdk.coders.KvCoder.decode(KvCoder.java:42)
I see that the answer to another, somewhat related question (Using TextIO.Write with a complicated PCollection type in Google Cloud Dataflow) says to map everything to strings, and use that to pass stuff around PCollections. Is that really the recommended way??
(Note: the actual code is in Scala, but I'm pretty sure it's not a Scala <=> Java issue so I've translated it into Java here.)
Update to include Scala code and more background:
So this is the actual exception itself (should have included this at the beginning):
java.lang.ClassCastException: cannot assign instance of scala.collection.immutable.HashMap$SerializationProxy to field com.example.schema.Schema.keyTypes of type scala.collection.immutable.Map in instance of com.example.schema.Schema
Where com.example.schema.Schema is:
case class Schema(id: String, keyTypes: Map[String, Type])
And lastly, the SchemaCoder is:
class SchemaCoder extends com.google.cloud.dataflow.sdk.coders.CustomCoder[Schema] {
def decode(inputStream: InputStream, context: Context): Schema = {
val ois = new ObjectInputStream(inputStream)
val id: String = ois.readObject().asInstanceOf[String]
val javaMap: java.util.Map[String, Type] = ois.readObject().asInstanceOf[java.util.Map[String, Type]]
ois.close()
Schema(id, javaMap.asScala.toMap)
}
def encode(schema: Schema, outputStream: OutputStream, context: Context): Unit = {
val baos = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(baos)
oos.writeObject(schema.id)
val javaMap: java.util.Map[String, Type] = schema.keyTypes.asJava
oos.writeObject(javaMap)
oos.close()
val encoded = new String(Base64.encodeBase64(baos.toByteArray()))
outputStream.write(encoded.getBytes())
}
}
====
Edit2: And here's what ToKVTransform actually looks like:
class SchemaExtractorTransform extends PTransform[PCollection[String], PCollection[Schema]] {
class InferSchemaFromStringWithKeyFn extends DoFn[String, KV[String, Schema]] {
override def processElement(c: DoFn[String, KV[String, Schema]]#ProcessContext): Unit = {
val line = c.element()
inferSchemaFromString(line)
}
}
class GetFirstFn extends DoFn[KV[String, java.lang.Iterable[Schema]], Schema] {
override def processElement(c: DoFn[KV[String, java.lang.Iterable[Schema]], Schema]#ProcessContext): Unit = {
val idAndSchemas: KV[String, java.lang.Iterable[Schema]] = c.element()
val it: java.util.Iterator[Schema] = idAndSchemas.getValue().iterator()
c.output(it.next())
}
}
override def apply(inputLines: PCollection[String]): PCollection[Schema] = {
val schemasWithKey: PCollection[KV[String, Schema]] = inputLines.apply(
ParDo.named("InferSchemas").of(new InferSchemaFromStringWithKeyFn())
)
val keyed: PCollection[KV[String, java.lang.Iterable[Schema]]] = schemasWithKey.apply(
GroupByKey.create()
)
val schemasOnly: PCollection[Schema] = keyed.apply(
ParDo.named("GetFirst").of(new GetFirstFn())
)
schemasOnly
}
}
This problem doesn't reproduce in Java; Scala is doing something differently with types that breaks Dataflow coder inference. To work around this, you can call setCoder on a PCollection to set its Coder explicitly, such as
schemasWithKey.setCoder(KvCoder.of(StringUtf8Coder.of(), SchemaCoder.of());
Here's the Java version of your code, just to make sure that it's doing approximately the same thing:
public static class SchemaExtractorTransform
extends PTransform<PCollection<String>, PCollection<Schema>> {
class InferSchemaFromStringWithKeyFn extends DoFn<String, KV<String, Schema>> {
public void processElement(ProcessContext c) {
c.output(KV.of(c.element(), new Schema()));
}
}
class GetFirstFn extends DoFn<KV<String, java.lang.Iterable<Schema>>, Schema> {
private static final long serialVersionUID = 0;
public void processElement(ProcessContext c) {
c.output(c.element().getValue().iterator().next());
}
}
public PCollection<Schema> apply(PCollection<String> inputLines) {
PCollection<KV<String, Schema>> schemasWithKey = inputLines.apply(
ParDo.named("InferSchemas").of(new InferSchemaFromStringWithKeyFn()));
PCollection<KV<String, java.lang.Iterable<Schema>>> keyed =
schemasWithKey.apply(GroupByKey.<String, Schema>create());
PCollection<Schema> schemasOnly =
keyed.apply(ParDo.named("GetFirst").of(new GetFirstFn()));
return schemasOnly;
}
}

Resources