You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ExoPlayer no longer Mockable. Unit test passes with 1.8.0 but fails with 1.9.0 #2985
This jnm junit test fails when I update from 1.8.0 to 1.9.0:
import androidx.media3.exoplayer.ExoPlayer
import io.mockk.mockk
import org.junit.Test
/**
* Test to demonstrate MockK compatibility issue with Media3 1.9.0
*
* PASSES with Media3 1.8.0
* FAILS with Media3 1.9.0
*/
class MockKMedia3IssueTest {
@Test
fun testExoPlayerMocking() {
val mock = mockk<ExoPlayer>()
println("Mock created successfully: ${mock::class.java}")
}
}
The stack trace:
java.lang.ExceptionInInitializerError
at jdk.internal.reflect.GeneratedSerializationConstructorAccessor16.newInstance(Unknown Source)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:48)
at io.mockk.proxy.jvm.ObjenesisInstantiator.instanceViaObjenesis(ObjenesisInstantiator.kt:76)
at io.mockk.proxy.jvm.ObjenesisInstantiator.instantiateViaProxy(ObjenesisInstantiator.kt:67)
at io.mockk.proxy.jvm.ObjenesisInstantiator.instance(ObjenesisInstantiator.kt:30)
at io.mockk.proxy.jvm.ProxyMaker.instantiate(ProxyMaker.kt:77)
at io.mockk.proxy.jvm.ProxyMaker.proxy(ProxyMaker.kt:46)
at io.mockk.impl.instantiation.JvmMockFactory.newProxy(JvmMockFactory.kt:34)
at io.mockk.impl.instantiation.AbstractMockFactory.newProxy$default(AbstractMockFactory.kt:24)
at io.mockk.impl.instantiation.AbstractMockFactory.mockk(AbstractMockFactory.kt:59)
at com.roku.mobile.videoplayer.impl.MockKMedia3IssueTest.testExoPlayerMocking(MockKMedia3IssueTest.kt:39)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
Caused by: java.lang.NullPointerException: Cannot invoke "String.length()" because "string" is null
at com.google.common.base.Ascii.toLowerCase(Ascii.java:409)
at androidx.media3.common.util.Util.isRunningOnEmulator(Util.java:474)
at androidx.media3.exoplayer.ExoPlayer.<clinit>(ExoPlayer.java:1283)
... 14 more
changed the title [-]Unit test passes with 1.8.0 but fails with 1.9.0[/-][+]ExoPlayer no longer Mockable. Unit test passes with 1.8.0 but fails with 1.9.0[/+]on Jan 5, 2026
From the stack trace it says that Build.DEVICE is null.
I think the tests should actually run with Roboletcric because ExoPlayer is designed to run on Android. I don't think it can be expected this works on any JVM/Platform.
Build.DEVICE is guaranteed to be non-null via CTS, so I agree with @marcbaechinger that assuming it is non-null is reasonable in a test and you should be running tests either in Robolectric, on an emulator/real device, or some other 'realistic enough' Android environment (which shouldn't violate CTS guarantees).
Also: Please don't mock ExoPlayer (or any Player implementation). You almost certainly have not correctly implemented the enormous & fiddly Player implementation correctly in your mock - which means your tests are not realistic, and therefore likely fairly useless for catching issues before they occur in production.
Please either use:
A real ExoPlayer with a fake clock and TestPlayerRunHelper
This is the most realistic option - you can see lots of examples in our code base.
If you need to create specific scenarios that are hard to recreate with a real player (e.g. errors or edge cases) you can use FakePlayer
From the stack trace it says that Build.DEVICE is null.
I think the tests should actually run with Roboletcric because ExoPlayer is designed to run on Android. I don't think it can be expected this works on any JVM/Platform.
Thanks for the quick response, Marc. I completely understand that ExoPlayer is fundamentally an Android library.
However, I wanted to raise that this specific change seems to conflict with the broader Android testing evolution that the platform team has been driving. The issue isn't that we expect full JVM compatibility, but rather that this breaks existing unit test patterns that were working fine with Media3 1.8.0.
The community migration away from Robolectric-first testing has been significant - our CI runs thousands of JUnit tests in seconds vs the minutes Robolectric would require. While we certainly use Robolectric for integration tests, this particular case seems like it could be addressed with a simple defensive check.
Since isRunningOnEmulator() is marked @UnstableApi, would you consider a null-safe implementation?
StringdeviceName = Build.DEVICE;
if (deviceName == null) returnfalse; // Not Android environmentdeviceName = Ascii.toLowerCase(deviceName);
This preserves the intended functionality while maintaining compatibility with existing test suites. I recognize you have deep insight into the Android ecosystem's direction - just wanted to share the impact we're seeing.
Please can you quote the part of this page that discourages Robolectric?
The page seems to be encouraging JVM tests over instrumentation ones. Robolectric tests run on the JVM (rather than a real/emulated android device). So I read the page as encouraging Robolectric.
@icbaker you know what, they where lazy references (thanks AI!). Sorry about that.
Please can you quote the part of this page that discourages Robolectric?
It's not that Robolectric is discouraged. It's that JVM unit testing should be possible without the Android framework getting in the way. (such as the creation of unitTests.returnDefaultValues from the reference https://developer.android.com/training/testing/local-tests#error)
The YT video explains the testing pyramid and testing cycle, it doesn't explicitly call out moving away from Robolectic, just calls out fast feedback and the fastest tests you can write are better.
We have a suite of integration tests, we use Espresso for on device acceptance tests also.
This report was giving the simplest reproducer possible just to show the reproduction, not to show what I am testing, or the range or ways to test.
The actual case is analytics/performance monitoring where we get certain fields off of ExoPlayer and manipulate them. Therefore these tests are not about ExoPlayer itself and thats why I only wanted to mock the ExoPlayer interface.
I'm only talking from my own memories of historical Android that this used to happen a lot more often, and Android was very very hard to unit test and we had to use Robolectric. Now we don't have to use Robolectic but I got the same surprise when going to mock ExoPlayer.
The solution can just be the same as it used to be, I wrap ExoPlayer in something I control so I don't have to worry about it. (The alternative to using Robolectric.)
But I just wanted to raise it because I don't believe it's good practice to have expectations of static fields existence, and unmockable static initializers, I thought it was a good opportunity for change, being an unstable api.
Thanks for the extra detail - I can see how tests with only incidental use of ExoPlayer and similar interfaces can benefit from avoiding the bloat of the full Robolectric. I wasn't really aware of this bit of magic:
When you execute local unit tests, the Android Gradle Plug-in includes a library that contains all the APIs of the Android framework, correct to the version used in your project. The library holds all the public methods and classes of those APIs, but the code inside the methods has been removed. If any of the methods are accessed, the test throws an exception.
Since nearly all our tests do require Robolectric as we're that bit closer to the platform - I assumed that Android stubs were always provided as part of using Robolectric.
I discussed this a bit with Marc. Neither of us like adding a null check for Build.VERSION that should "never be null" - since it pollutes the production code with 'unecessary' checks that only exist due to testing limitations.
I have instead taken a different approach: Move these constants from ExoPlayer to ExoPlayer.Builder. I believe this is where they belong anyway, because they are default values used if you don't call a setter on the builder (and this is supported by them only being used from inside ExoPlayer.Builder). This means they are no longer resolved when mocking ExoPlayer (since the ExoPlayer.Builder class is not loaded) and so mocking works again. I've sent this change for internal review, including a test that checks ExoPlayer (and Player) can be mocked without using Robolectric - so we shouldn't backslide on this in future.
Since this change will break the compilation of anyone using the ExoPlayer.DEFAULT_STUCK_PLAYING_DETECTION_TIMEOUT_MS constants we won't include it in a 1.9.1 bugfix but in 1.10.0 instead.
Great, thanks for listening and thinking it through 💯
An unrelated feature enhancement could be to somehow expose isRunningOnEmulator in the ExoPlayer builder, or as an injectable, so that clients could pass a flag/class that could override the library code for figuring it out.
I saw Pirsky's response, which would also be a valid work around, but we don't want to mockStatic's as they can bleed into other tests and its also mocking internals (which can change).
Activity
blundell commentedon Jan 5, 2026
Seems like the line:
Was added to
media3-exoplayer-1.9.0-sources.jar!/androidx/media3/exoplayer/ExoPlayer.java( It does not exist in
media3-exoplayer-1.8.0-sources.jar!/androidx/media3/exoplayer/ExoPlayer.java. )and
isRunningOnEmulator()makes the assumption of its either going to run on an Emulator or a Real Android Device, never as a JVM unit test:It could be fixed by returning
falseif running on a JVM.Your choice of:
Option 1: Null Check (Simplest)
Option 2: Try-Catch (More Defensive)
Option 3: Check Android Environment First
[-]Unit test passes with 1.8.0 but fails with 1.9.0[/-][+]ExoPlayer no longer Mockable. Unit test passes with 1.8.0 but fails with 1.9.0[/+]marcbaechinger commentedon Jan 5, 2026
Thanks for your report.
From the stack trace it says that
Build.DEVICEis null.I think the tests should actually run with Roboletcric because
ExoPlayeris designed to run on Android. I don't think it can be expected this works on any JVM/Platform.icbaker commentedon Jan 5, 2026
Build.DEVICEis guaranteed to be non-null via CTS, so I agree with @marcbaechinger that assuming it is non-null is reasonable in a test and you should be running tests either in Robolectric, on an emulator/real device, or some other 'realistic enough' Android environment (which shouldn't violate CTS guarantees).Also: Please don't mock
ExoPlayer(or anyPlayerimplementation). You almost certainly have not correctly implemented the enormous & fiddlyPlayerimplementation correctly in your mock - which means your tests are not realistic, and therefore likely fairly useless for catching issues before they occur in production.Please either use:
ExoPlayerwith a fake clock andTestPlayerRunHelperFakePlayerblundell commentedon Jan 5, 2026
Thanks for the quick response, Marc. I completely understand that ExoPlayer is fundamentally an Android library.
However, I wanted to raise that this specific change seems to conflict with the broader Android testing evolution that the platform team has been driving. The issue isn't that we expect full JVM compatibility, but rather that this breaks existing unit test patterns that were working fine with Media3 1.8.0.
The community migration away from Robolectric-first testing has been significant - our CI runs thousands of JUnit tests in seconds vs the minutes Robolectric would require. While we certainly use Robolectric for integration tests, this particular case seems like it could be addressed with a simple defensive check.
Since
isRunningOnEmulator()is marked@UnstableApi, would you consider a null-safe implementation?This preserves the intended functionality while maintaining compatibility with existing test suites. I recognize you have deep insight into the Android ecosystem's direction - just wanted to share the impact we're seeing.
References (in case helpful for the cause):- Official guide prioritizing JUnit over Robolectric https://developer.android.com/training/testing/local-tests- Where Google acknowledged the need for faster unit tests https://www.youtube.com/watch?v=4m2yYSTdvIg- Multiple posts about moving away from Robolectric-first approach https://android-developers.googleblog.com/search/label/testingThanks for considering it!
icbaker commentedon Jan 5, 2026
Please can you quote the part of this page that discourages Robolectric?
The page seems to be encouraging JVM tests over instrumentation ones. Robolectric tests run on the JVM (rather than a real/emulated android device). So I read the page as encouraging Robolectric.
This link has no results, it just says
No results for "testing"(which starts to make me suspect it was hallucinated by an LLM).I didn't watch this video. Please give me a timestamp (since I'm slightly suspicious of these citations at this point).
blundell commentedon Jan 5, 2026
@icbaker you know what, they where lazy references (thanks AI!). Sorry about that.
It's not that Robolectric is discouraged. It's that JVM unit testing should be possible without the Android framework getting in the way. (such as the creation of
unitTests.returnDefaultValuesfrom the reference https://developer.android.com/training/testing/local-tests#error)The YT video explains the testing pyramid and testing cycle, it doesn't explicitly call out moving away from Robolectic, just calls out fast feedback and the fastest tests you can write are better.
We have a suite of integration tests, we use Espresso for on device acceptance tests also.
This report was giving the simplest reproducer possible just to show the reproduction, not to show what I am testing, or the range or ways to test.
The actual case is analytics/performance monitoring where we get certain fields off of ExoPlayer and manipulate them. Therefore these tests are not about ExoPlayer itself and thats why I only wanted to mock the ExoPlayer interface.
I'm only talking from my own memories of historical Android that this used to happen a lot more often, and Android was very very hard to unit test and we had to use Robolectric. Now we don't have to use Robolectic but I got the same surprise when going to mock ExoPlayer.
The solution can just be the same as it used to be, I wrap ExoPlayer in something I control so I don't have to worry about it. (The alternative to using Robolectric.)
But I just wanted to raise it because I don't believe it's good practice to have expectations of static fields existence, and unmockable static initializers, I thought it was a good opportunity for change, being an unstable api.
lol:
🙏
Pirksy commentedon Jan 5, 2026
Hi @blundell , just put
before
val mock = mockk<ExoPlayer>()icbaker commentedon Jan 5, 2026
Thanks for the extra detail - I can see how tests with only incidental use of
ExoPlayerand similar interfaces can benefit from avoiding the bloat of the full Robolectric. I wasn't really aware of this bit of magic:Since nearly all our tests do require Robolectric as we're that bit closer to the platform - I assumed that Android stubs were always provided as part of using Robolectric.
I discussed this a bit with Marc. Neither of us like adding a null check for
Build.VERSIONthat should "never be null" - since it pollutes the production code with 'unecessary' checks that only exist due to testing limitations.I have instead taken a different approach: Move these constants from
ExoPlayertoExoPlayer.Builder. I believe this is where they belong anyway, because they are default values used if you don't call a setter on the builder (and this is supported by them only being used from insideExoPlayer.Builder). This means they are no longer resolved when mockingExoPlayer(since theExoPlayer.Builderclass is not loaded) and so mocking works again. I've sent this change for internal review, including a test that checksExoPlayer(andPlayer) can be mocked without using Robolectric - so we shouldn't backslide on this in future.Since this change will break the compilation of anyone using the
ExoPlayer.DEFAULT_STUCK_PLAYING_DETECTION_TIMEOUT_MSconstants we won't include it in a 1.9.1 bugfix but in 1.10.0 instead.blundell commentedon Jan 5, 2026
Great, thanks for listening and thinking it through 💯
An unrelated feature enhancement could be to somehow expose
isRunningOnEmulatorin the ExoPlayer builder, or as an injectable, so that clients could pass a flag/class that could override the library code for figuring it out.I saw Pirsky's response, which would also be a valid work around, but we don't want to mockStatic's as they can bleed into other tests and its also mocking internals (which can change).
Move `ExoPlayer.DEFAULT_` constants to `ExoPlayer.Builder`
[video_player] Update Android to exoplayer 1.9.1 (#10904)