Unit testing: NoSuchMethodError while calling Notification.setLatestEventInfo() - android

Feel free to improve the title I'm a little uncreative in this special case.
I am implementing a unit test for checking notifications, this is hardly possible I know, but I want to check how far I can automatism it.
I cannot test this simple line of code:
Notification test = new NotificationCompat.Builder(context).build();
The reason is stupid and simple in one. This code here will been executed internally:
public Notification build(Builder b, BuilderExtender extender) {
Notification result = b.mNotification;
result.setLatestEventInfo(b.mContext, b.mContentTitle,
b.mContentText, b.mContentIntent);
[...]
I'm getting this exception:
Caused by: java.lang.NoSuchMethodError: android.app.Notification.setLatestEventInfo(Landroid/content/Context;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Landroid/app/PendingIntent;)V
at android.support.v4.app.NotificationCompat$NotificationCompatImplBase.build(NotificationCompat.java:479)
at android.support.v4.app.NotificationCompat$Builder.build(NotificationCompat.java:1561)
It is not hard to guess that the Google guys call here a method which was removed (or more properly annotated with #hide) in the Android Marshmallow SDK. I have verified it that call is missing the the newest documentation, but it was introduced in API 1 AFIK.
How can I work around that?
Things I tried and got stuck:
Overriding the callback, and mock that method without invoking that call:
I got it managed to get that Class<?> with the callback, but with which method I can override a hole method? I mean I need to patch the call it I cannot just mock it.
Injecting that call, but how? I can just override it and not adding it.
Suppressing the call with:
PowerMockito.spy(Notification.class);
PowerMockito.suppress(PowerMockito.method(Notification.class, "setLatestEventInfo", Context.class, CharSequence.class, CharSequence.class, PendingIntent.class));
Does not work ether since I try to kick a non existing method.
Change the target SDK for this test, but how can I do it?

The solution is easier than expected. I missed that by default Build.VERSION.SDK_INT has the value 0 since it cannot read the real value. So that support library calls it on just that platforms where this method exists.
With the help of this answer. I just had to add this code:
setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 23);
And my the codes works.
Well it still crashes somewhere else, but the notification is created. Wohoo!
And the actual function:
public static void setFinalStatic(Field field, Object newValue) throws IllegalAccessException, NoSuchFieldException
{
field.setAccessible(true);
// remove final modifier from field
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}

Related

Android Unit Testing with Mockito

I have a ViewModel in which there is a method which has the following line of code:
billDate.set(!TextUtils.isEmpty(SampleApp.getInstance().getAccountManager().getDueDate()) ?
String.format(SampleApp.getInstance().getApplicationContext().getString(R.string.due),
SampleApp.getInstance().getAccountManager().getBillingDueDate()) :
SampleApp.getInstance().getApplicationContext().getString(R.string.missing_due_date));
I have a test class using Mockito to test the different methods in ViewModel. But it is failing with NullPointerException at this line:
String.format(SampleApp.getInstance().getApplicationContext().getString(R.string.due),
Below is the log:
java.lang.NullPointerException
at java.util.regex.Matcher.getTextLength(Matcher.java:1283)
at java.util.regex.Matcher.reset(Matcher.java:309)
at java.util.regex.Matcher.<init>(Matcher.java:229)
at java.util.regex.Pattern.matcher(Pattern.java:1093)
at java.util.Formatter.parse(Formatter.java:2547)
at java.util.Formatter.format(Formatter.java:2501)
at java.util.Formatter.format(Formatter.java:2455)
at java.lang.String.format(String.java:2940)
While running a test case, I see the log showing some error related to Pattern
Can somebody suggest, how to test the String.format() method?
First of all, you should not be importing android view packages into your ViewModel. So skip using things like TextUtils inside ViewModels.
As to the getApplicationContext().getString(), create an interface for this. Something like:
interface StringProvider {
String getString(int resource);
}
Then pass that interface in your ViewModel constructor and use that to get the string you want.
When you initialize the ViewModel, you can pass a concrete implementation of StringProvider like this:
class StringProviderImpl implements StringProvider {
String getString(int resource) {
return SampleApp.getInstance().getApplicationContext().getString(resource);
}
}
This way, for your unit tests, you can just mock StringProvider and don't have to worry about dealing with contexts inside your ViewModel and the related test code.
You don't need to test the String.format method. That is not your code, and your goal should be to test your own code. But your code is using that method, so you need to test your code. This is the part you are trying to validate or mock out as I understand it:
String.format(SampleApp.getInstance().getApplicationContext().getString(R.string.due), SampleApp.getInstance().getAccountManager().getBillingDueDate())
which makes several calls to SampleApp to get an instance. Since those calls to SampleApp.getInstance are static method calls, you won't be able to mock them out. There isn't enough code posted to know what SampleApp is or what SampleApp.getInstance() returns or to know if any of the subsequent calls on that instance are returning null, but one of them is. So I think to solve this you need to look at the what the getInstance method returns. If you can't touch that code and you're hoping to only modify your test classes, you may not be able to test this with mockito due to the static method.
But otherwise you will need to build a way for your tests so the call to SampleApp.getInstance returns a mock object as the instance instead of whatever real instance I presume it is returning now. Then you can mock out the subsequent methods like getApplicationContext and getString to make them return canned responses so that the string.format call will not fail on a null input.
One note of caution--if you do end up making the static getInstance method return a mock, but sure you have proper cleanup when your test is done to set it back to what it was returning originally so you don't inadvertently modify something that might cause another unrelated unit test to fail. That is always a risk if you change something returned by a static method in a unit test since you are effectively changing it for all tests.
Considering that the test fails after the AccountManager was already used, you should have set up the SampleApp as a mock or fake already.
SampleApp app = SampleApp.getInstance()
AccountManager am = app.getAccountManager();
Context context = app.getApplicationContext();
billDate.set(!TextUtils.isEmpty(am.getDueDate()) ?
String.format(context.getString(R.string.due), am.getBillingDueDate()) :
context.getString(R.string.missing_due_date);
Now you only need to make sure to mock the Context you provide with with app.getApplicationContext() or the SampleApp itself, if you use app.getString() directly.
doReturn(dueFormatString).when(context).getString(R.string.due);
doReturn(dueMissingString).when(context).getString(R.string.missing_due_date);
But in general you should abstract the Context away. Not using it will simplify your code and therefore your testing a lot.
Also consider using context.getString() instead of String.format() for formatting a string you load from a resource. It's as easy as adding the format arguments as parameters to the call.
context.getString(R.string.due, am.getBillingDueDate())

LifecycleObserver produce exception with methods that use newer APIs

My ViewModel class implements LifecycleObserver.
When I call fragment.lifecycle.addObserver(this) it produces exception.
Caused by: java.lang.IllegalArgumentException: The observer class has some methods that use newer APIs which are not available in the current OS version. Lifecycles cannot access even other methods so you should make sure that your observer classes only access framework classes that are available in your min API level OR use lifecycle:compiler annotation processor.
Strange, that firstly it was working fine, but not long ago this exception has appeared. I've found, that audioFocusRequest is cause of this bug.
private val audioFocusRequest by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setOnAudioFocusChangeListener(this)
.build() else throw RuntimeException("Can't be done for Android API lower than 26")
}
Does anybody know how it can be fixed?
UPD
Tried to use annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycle_version", but got compilation error:
(decided to paste screenshot, because whole logs are quite big)
UPD 2
At the end I've decided to delete audioFocusRequest field and to use old deprecated method - requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) instead of recommended requestAudioFocus(#NonNull AudioFocusRequest focusRequest)
It helped me to make code working again, so it can be solution. But I didn't find answer - why this problem had appeared. It strange because code used to be working before.
So problem has been solved but question still stays unanswered
Try to use kapt "androidx.lifecycle:lifecycle-compiler:2.0.0"
The class which implements LifecycleObserver has some method, which has parameters with type that only exist for higher APIs.
Your variables (i guess) and function parameters must exist on all APIs even function is not called (maybe this is requirement for classes who implement LifecycleObserver).
A possible solution is to change function parameter type to Any (kotlin) or Object (Java) and cast it to appropriate type inside function.
I have to remove this set method on SpinnerView: lifecycleOwner = viewLifecycleOwner
I was able to fix this by moving the offending methods into another class, but still called from my LifecycleObserver. After reading the error message again:
Caused by: java.lang.IllegalArgumentException: The observer class has some methods that use newer APIs which are not available in the current OS version. Lifecycles cannot access even other methods so you should make sure that your observer classes only access framework classes that are available in your min API level OR use lifecycle:compiler annotation processor.
It seems as though no methods or objects are allowed in the class extending LifecycleObserver if they don't exist in the device's OS, even if they are wrapped in an SDK version check and never accessed.

How to make IDE to understand that, the object exactly not null after method call

I am using android studio with Kotlin language
and when I call the function above, its warning to check null state of "obj"
How its possible to make IDE (Android Studio) to understand that after some method call, the (passed) object exactly not null... thanks
Instead of an opaque isNotNull method call, which Kotlin can't (and shouldn't) reason about, use one of the idioms native to the language, such as:
value?.let {
val response = it.response
...
}

Xamarin LocationManager.RequestSingleUpdate always throws IllegalArgumentException

For some reason whenever I implement ILocationListener and implement the necessary interface methods this call always crashes on me:
LocMgr.RequestSingleUpdate(provider, this, null);
It throws java.lang.IllegalArgumentException: invalid listener: null although it's obviously impossible for this to be null. Does anyone have any suggestions on how to deal with this?
There were two issues here.
I needed to inherit from Java.Lang.Object
I needed to make sure I wasn't providing new implementations of Dispose or Handle.
Once I removed these the function call worked as expected.

Android unit testing and interfaces

I have been having quite a bit of trouble implementing unit testing on the Android. As a simple test, I've been trying to match a string retrieved from string resources:
String myString = myActivity.getResources().getString(R.string.testString));
However, when unit testing this invariably results in a null pointer exception. This includes robolectric as well as the Junit implementation delivered with the Android sdk.
One possible solution is to approach the retrieval of resources in a manner similar to a data access object. That is, create an interface through which string resources would be accessed. This would allow me to mock access the string resource. Similarly, I could separate the non-android dependent behavior of, say, an Activity, into a separate pojo class. This would allow me to run unit tests using standard Java testing tools. In fact, I could potentially delegate any Android infrastructure related activity to an interface.
This seems like a lot of jumping through hoops to get to unit testing. Is it worth it? Is there a more viable approach?
It turned out, the problem was that the activity has to be gotten in the actual test method. So, for example, my method now looks like this:
public void testGetActivityResourceString() {
Activity myActivity = this.getActivity();
String myString = myActivity.getResources().getString(R.string.hello);
Assert.assertNotNull(myString);
}
Whereas before I was creating activity in setup. This giveaway was in the docs:
"For each test method invocation, the Activity will not actually be created until the first time this method is called."
This was a real hassle to figure out. The example for HelloWorldTest doesn't work for the same reason.
Here's the full entry:
Public T getActivity ()
Since: API Level 3
Get the Activity under test, starting it if necessary.
For each test method invocation, the Activity will not actually be created until the first time this method is called.
If you wish to provide custom setup values to your Activity, you may call setActivityIntent(Intent) and/or setActivityInitialTouchMode(boolean) before your first call to getActivity(). Calling them after your Activity has started will have no effect.
NOTE: Activities under test may not be started from within the UI thread. If your test method is annotated with UiThreadTest, then your Activity will be started automatically just before your test method is run. You still call this method in order to get the Activity under test.
This works correctly:
public void testGetResourceString() {
assertNotNull(mActivity.getResources()
.getString(com.example.pkg.R.string.testString));
}
Because you haven't provided any of your code but only the getReousrces() line, I will guess what you are doing wrong:
you are not using the correct base class for your test, use ActivityInstrumentationTestCase2 because you need the system infrastructure
you are using the resources of your test project instead of your project under test, that's why in my example the id is com.example.pkg.R.string.testString.

Categories

Resources