Android Studio 3.5.3
Kotlin 1.3
I am trying to test some simple code but I keep getting the following exception:
IllegalStateException: gsonWrapper.fromJson<Map…ring, String>>() {}.type) must not be null
I am using the spy and mocking the return so it will return a null. As I want to test the error path.
Not sure if I am doing something wrong with my stubbing or not. But can’t seem to resolve this exception.
Using a wrapper class to wrap the gson implementation and spying on this in the test
public class GsonWrapper implements IGsonWrapper {
private Gson gson;
public GsonWrapper(Gson gson) {
this.gson = gson;
}
@Override public <T> T fromJson(String json, Type typeOf) {
return gson.fromJson(json, typeOf);
}
}
Implementation of my class that is under test
class MoviePresenterImp(
private val gsonWrapper: IGsonWrapper) : MoviePresenter {
private companion object {
const val movieKey = "movieKey"
}
override fun saveMovieState(movieJson: String) {
val movieMap = serializeStringToMap(movieJson)
when (movieMap.getOrElse(movieKey, {""})) {
/* do something here */
}
}
// Exception return from this method
private fun serializeStringToMap(ccpaStatus: String): Map<String, String> =
gsonWrapper.fromJson<Map<String, String>>(ccpaStatus, object : TypeToken<Map<String, String>>() {}.type) // Exception
}
The actual test class, just keeping everything simple
class MoviePresenterImpTest {
private lateinit var moviePresenterImp: MoviePresenterImp
private val gsonWrapper: GsonWrapper = GsonWrapper(Gson())
private val spyGsonWrapper = spy(gsonWrapper)
@Before
fun setUp() {
moviePresenterImp = MoviePresenterImp(spyGsonWrapper)
}
@Test
fun `should not save any movie when there is an error`() {
// Arrange
val mapType: Type = object : TypeToken<Map<String, String>>() {}.type
whenever(spyGsonWrapper.fromJson<Map<String, String>>("{\"movie\":\"movieId\"}", mapType)).thenReturn(null)
// Act
moviePresenterImp.saveMovieState("{\"movie\":\"movieId\"}")
// Assert here
}
}
Many thanks for any suggestions,
It depends on what you want to achieve. Do you want to allow MoviePresenterImp.serializeStringToMap
to return null
? At the moment it is not possible and that’s what you are testing in your unit test:
-
what will happen when
gsonWrapper.fromJson
returnsnull
? -
serializeStringToMap
will throw an exception because its return type is declared to be non-nullable (Kotlin adds a null-check under the hood).
In fact,spyGsonWrapper.fromJson
only returns null
if gson.fromJson
returns null
. According to the Gson’s java docs, it may happen only if the json
argument is null
(if json
is invalid the method throws JsonSyntaxException
). So you should either:
- check if the
json
parameter isnull
in thespyGsonWrapper.fromJson
and throw IllegalArgumentException if it is. This will ensure that the method never returnsnull
(btw. you could add a@NotNull
annotation, see nullability-annotations). You can keepserializeStringToMap
as it is but you need to change the test, because it makes no sense anymore. - if you prefer returning
null
instead of throwing an exception, you need to change the MoviePresenterImp.serializeStringToMap as suggested by @duongdt3
Here is an example test:
class MoviePresenterImpTest {
private lateinit var moviePresenter: MoviePresenterImp
private lateinit var spyGsonWrapper: GsonWrapper
@Rule @JvmField
var thrown = ExpectedException.none();
@Before
fun setUp() {
spyGsonWrapper = Mockito.mock(GsonWrapper::class.java)
moviePresenter = MoviePresenterImp(spyGsonWrapper)
}
@Test
fun `should not save any movie when GsonWrapper throws an error`() {
// Given
Mockito.`when`(spyGsonWrapper.fromJson<Map<String, String>>(anyString(), any(Type::class.java)))
.thenThrow(JsonSyntaxException("test"))
// Expect
thrown.expect(JsonSyntaxException::class.java)
// When
moviePresenter.saveMovieState("{\"movie\":\"movieId\"}")
}
// Or without mocking at all
@Test
fun `should not save any movie when Gson throws error`() {
// Given
moviePresenter = MoviePresenterImp(GsonWrapper(Gson()))
// Expect
thrown.expect(JsonSyntaxException::class.java)
// When
moviePresenter.saveMovieState("Some invalid json")
}
// If you want to perform additional checks after an exception was thrown
// then you need a try-catch block
@Test
fun `should not save any movie when Gson throws error and `() {
// Given
moviePresenter = MoviePresenterImp(GsonWrapper(Gson()))
// When
try {
moviePresenter.saveMovieState("Some invalid json")
Assert.fail("Expected JsonSyntaxException")
} catch(ex : JsonSyntaxException) {}
// Additional checks
// ...
}
}
Answer:
I found problems here:
You must use nullable Map? instead of non-null Map in MoviePresenterImp (Kotlin code), because in Unit Test class, you spy gsonWrapper, and force method ‘spyGsonWrapper.fromJson’ return null.
It OK now.
fun saveMovieState(movieJson: String) {
val movieMap = serializeStringToMap(movieJson)
when (movieMap?.getOrElse(movieKey, { "" })) {
/* do something here */
}
}
// Exception return from this method
private fun serializeStringToMap(ccpaStatus: String): Map<String, String>? {
val type: Type =
object : TypeToken<Map<String, String>>() {}.type
return gsonWrapper.fromJson(ccpaStatus, type) // Exception
}
Answer:
With your setup you’re looking for emptyMap()
instead of null
whenever(spyGsonWrapper.fromJson<Map<String, String>>("{\"movie\":\"movieId\"}", mapType))
.thenReturn(emptyMap())
This will comply with the signature, as it’s non-null
fun serializeStringToMap(ccpaStatus: String): Map<String, String>
Also it’ll enter the else-block within the movieMap.getOrElse()
call.
Tags: androidandroid, class, exception