Kotlin Coroutines Unit Testing - unit-testing

Im trying to test this suspend function:
suspend fun <T> getResult(call: suspend () -> Response<T>): Resource<T> {
val response = call()
val body = response.body()
val code = response.code()
if (response.isSuccessful) {
Log.d(Cybrid.instance.tag, "Data: ${response.code()} - ${response.body()}")
return Resource.success(body!!, code)
} else if (response.code() == HTTP_UNAUTHORIZED || response.code() == HTTP_FORBIDDEN) {
Cybrid.instance.let { cybrid ->
cybrid.listener.let {
cybrid.invalidToken = true
it?.onTokenExpired()
}
}
Log.e(Cybrid.instance.tag, "Error - Something wrong with TOKEN : ${response.code()} ${response.message()}")
return Resource.error(response.message(), code=response.code())
} else {
Log.e(Cybrid.instance.tag, "Error - Other: ${response.code()} ${response.message()} :: ${response.raw()}")
return Resource.error(message = response.message(), data= response.body(), code= response.code())
}
}
With this Unit Test Case, all it cover except line 13 , I dont know how to cover the line!!!
#ExperimentalCoroutinesApi
#Test
fun get400ErrorServerTest() = runBlocking {
Cybrid.instance.setBearer("Bearer")
val pricesService = AppModule.getClient().createService(PricesApi::class.java)
val result = getResult { pricesService.listPrices() }
Assert.assertNotNull(result)
Assert.assertEquals(result.code, 400)
Assert.assertNull(result.data)
}
The coverage report says:
Some idea to get coverage for line 13 ???
Thnanks

Related

How can I collect values from a mutableSharedFlow in a unit test?

The following test doesn't pass. The string "before adding XXX" is never printed.
#Test
fun testFoo() = runBlocking {
val testCoroutineScope = TestCoroutineScope().apply {
pauseDispatcher() // This needs to be here because the actual test handles time.
}
val sharedFlow = MutableSharedFlow<Int>()
val values = mutableListOf<Int>()
println("before launch")
val job = testCoroutineScope.launch {
println("before collect")
sharedFlow.collect {
println("before adding $it")
values.add(it)
}
}
println("before emits")
sharedFlow.emit(1)
sharedFlow.emit(2)
testCoroutineScope.runCurrent()
assertEquals(mutableListOf(1, 2), values)
job.cancel()
}
I would like to have a way to emit values once the collect part is handled. I can't set a replay value, nor can I use an onSubscription because that would change the business logic.
Adding delay or yield don't seem to make any difference so it might not be a racing condition.
The test passes if pauseDispatcher() is commented.
My understanding was that emit would suspend until the collect lambda were called.
The way I solved it is via extraBufferCapacity and one more testCoroutineScope.runCurrent()
#Test
fun testFoo() = runBlocking {
val testCoroutineScope = TestCoroutineScope().apply {
pauseDispatcher()
}
val sharedFlow = MutableSharedFlow<Int>(
extraBufferCapacity = 2 // Without it, sharedFlow.emit won't have a space to save data. It will be collected
// next time there's a testCoroutineScope.runCurrent()
)
val values = mutableListOf<Int>()
println("before launch")
val job = testCoroutineScope.launch {
println("before collect")
sharedFlow.collect {
println("before adding $it")
values.add(it)
}
}
testCoroutineScope.runCurrent() // Allows the previous launch to start collecting
println("before emits")
sharedFlow.emit(1)
sharedFlow.emit(2)
testCoroutineScope.runCurrent()
assertEquals(mutableListOf(1, 2), values)
job.cancel()
}
UPDATE: Turns out my initial attempt wasn't bulletproof either, a race condition still existed, here's my revised solution, so far so good:
val msf = MutableSharedFlow<Int>()
runBlocking {
val job1 = GlobalScope.launch {
msf
.onStart {
println("${Thread.currentThread().name} - on start ")
}
.onEach { println("got $it") }
.collect()
}
val job2 = launch {
msf.subscriptionCount
.filter { it > 0 }
.onEach {
msf.emit(42)
cancel()
}
.collect()
}
delay(100)
job1.cancel()
println(job2)
}
println("${Thread.currentThread().name} - escaped!")
Old solution (incorrect):
runBlocking {
val msf = MutableSharedFlow<Int>()
val d = CompletableDeferred<Boolean>()
launch {
msf
.onStart {
d.complete(true)
}
.onEach { println("got $it") }
.collect()
}
d.join()
msf.emit(42)
}
using the invokeOnCompletion() handler cannot use suspended functions, using it with tryEmit() did not work for me (returns true but does nothing).

Coroutine testing fails with "This job has not completed yet"

I'm following Craig Russell's blog post about testing coroutines: https://craigrussell.io/2019/11/unit-testing-coroutine-suspend-functions-using-testcoroutinedispatcher/ but I can't get this test to pass:
#Test
fun multipleLaunch() = runBlockingTest {
var result = 0
val jobs = mutableListOf<Job>()
for (j in 0 until 10) {
val job = launch(testDispatcherProvider.io()) {
delay(1000)
result++
}
jobs.add(job)
}
jobs.forEach { job ->
job.join()
}
assertEquals(10, result)
}
Basically I'm launching a bunch of parallel jobs and want to get the result once all of them are finished.
I'm getting this, by now classical exception:
java.lang.IllegalStateException: This job has not completed yet
Please advise how to get this to work as intended.
My complete code:
class LaunchTest {
#get:Rule
var coroutinesTestRule = CoroutineTestRule()
val testDispatcherProvider = object : DispatcherProvider {
override fun io(): CoroutineDispatcher = coroutinesTestRule.testDispatcher
}
#Test
fun multipleLaunch() = runBlockingTest {
var result = 0
val jobs = mutableListOf<Job>()
for (j in 0 until 10) {
val job = launch(testDispatcherProvider.io()) {
delay(1000)
result++
}
jobs.add(job)
}
jobs.forEach { job ->
job.join()
}
assertEquals(10, result)
}
}
class CoroutineTestRule constructor(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
Solved.
I totally blame Android Studio's auto-completion for this. :)
I simply ran the wrong "runBlockingTest()".
Replace this line:
fun multipleLaunch() = runBlockingTest {
with this line:
fun multipleLaunch() = coroutinesTestRule.testDispatcher.runBlockingTest {
Instead of runBlockingTest{} I used runBlocking{} because of Issue 1204

LiveData unit test do not pass under coroutine and multithread because of return true instead of expected false

EnrollmentViewModelTest
#Test
fun getUserProfile() {
val responseSnapshot = this.javaClass.getResource("/userDetailResponse.json").readText()
val user = Gson().fromJson<User>(responseSnapshot, User::class.java)
val response = Response.success(user)
val deferred = CompletableDeferred<Response<User>>(response)
coEvery { userService.getUserDetail() } returns deferred
viewModel.getUserProfile()
assert(viewModel.loadingStatus.value != null)
assert(!UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(false, viewModel.loadingStatus.value!!)
}
Here is EnrollmentViewModel.kt
fun getUserProfile() {
loadingStatus.postValue(true)
job = launch {
callAsync {
userService.getUserDetail()
}.onSuccess { user ->
if (user != null) {
processUserDetails(user)
}
loadingStatus.postValue(false)
}.onError {
//
}.onException {
//
}
}
}
When I debug the test case, it show that the UITestUtil.getValue(viewModel.loadingStatus)!! is false.
But weirdly, the test case does not pass, and when I print the UITestUtil.getValue(viewModel.loadingStatus)!!. It is true.
It may related to the loadingStatus.postValue(true)
After I remove it, the print result is false.
But I do not know why.
object UITestUtil {
/**
* Gets the value of a LiveData safely.
*/
#Throws(InterruptedException::class)
fun <T> getValue(liveData: LiveData<T>): T? {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
liveData.removeObserver(this)
}
}
liveData.observeForever(observer)
latch.await(2, TimeUnit.SECONDS)
return data
}
}
Updated:
import com.google.gson.Gson
import kotlinx.coroutines.Deferred
import retrofit2.Response
import retrofit2.Response.success
data class VPlusResult<T : Any?>(
val response: Response<T>? = null,
val exception: Exception? = null
)
inline fun <T : Any> VPlusResult<T>.onSuccess(action: (T?) -> Unit): VPlusResult<T> {
if (response?.isSuccessful == true)
action(response.body())
return this
}
inline fun <T : Any> VPlusResult<T>.onRawSuccess(action: (response: Response<T>) -> Unit): VPlusResult<T> {
if (response?.isSuccessful == true)
action(response)
return this
}
inline fun <T : Any, TR : Any> VPlusResult<T>.map(action: (T?) -> (TR)) =
if (response?.isSuccessful == true) VPlusResult(success(action(response.body())))
else this as VPlusResult<TR>
inline fun <T : Any> VPlusResult<T>.onError(action: (String) -> Unit): VPlusResult<T> {
if (response?.isSuccessful != true) {
response?.errorBody()?.let {
action(it.string())
}
}
return this
}
inline fun <T : Any, reified G : Any> VPlusResult<T>.onErrorJson(action: (G) -> Unit): VPlusResult<T> {
if (response?.isSuccessful != true) {
response?.errorBody()?.let {
action(Gson().fromJson(it.string(), G::class.java))
}
}
return this
}
inline fun <T : Any> VPlusResult<T>.onRawError(action: (Response<T>?) -> Unit): VPlusResult<T> {
if (response?.isSuccessful != true) {
action(response)
}
return this
}
inline fun <T : Any?> VPlusResult<T>.onException(action: (Exception) -> Unit) {
exception?.let { action(it) }
}
inline fun <T : Any> VPlusResult<T>.onSadness(action: (String?) -> Unit): VPlusResult<T> {
onError {
action(it)
}.onException {
action(it.message)
}
return this
}
suspend fun <T : Any> callAsync(block: () -> Deferred<Response<T>>): VPlusResult<T> {
return try {
VPlusResult(block().await())
} catch (e: Exception) {
VPlusResult(exception = e)
}
}
Updated 2:
adding either of the following statements before assert statement will get the value false, which passes the test case.
coVerify { userService.getUserDetail() }
coVerify { viewModel.processUserDetails(user) }
Update 3:
fun loadAllTasksFromRepository_loadingTogglesAndDataLoaded()
in https://github.com/android/architecture-samples
Also works, but I try to introduce into my codes, it also failed.
#ExperimentalCoroutinesApi
#Test
fun getUserProfile4Using() {
// using TestCoroutineScope in kotlinx.coroutines.test
// Pause dispatcher so we can verify initial values
mainCoroutineRule.pauseDispatcher()
val responseSnapshot = this.javaClass.getResource("/userDetailResponse.json").readText()
val user = Gson().fromJson<User>(responseSnapshot, User::class.java)
val response = Response.success(user)
val deferred = CompletableDeferred<Response<User>>(response)
// coEvery { userService.getUserDetail() } returns deferred
viewModel.getUserProfile()
assert(viewModel.loadingStatus.value != null)
// verify { viewModel.processUserDetails(user) }
print(viewModel.loadingStatus.value)
assert(UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(true, viewModel.loadingStatus.value!!)
// Execute pending coroutines actions
mainCoroutineRule.resumeDispatcher()
print(viewModel.loadingStatus.value!!)
assert(!UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(false, viewModel.loadingStatus.value!!)
}
I'd say you should probably rewrite your test using runBlockingTest(https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/)
fun getUserProfile() = runBlockingTest{
val responseSnapshot = this.javaClass.getResource("/userDetailResponse.json").readText()
val user = Gson().fromJson<User>(responseSnapshot, User::class.java)
val response = Response.success(user)
val deferred = CompletableDeferred<Response<User>>(response)
coEvery { userService.getUserDetail() } returns deferred
viewModel.getUserProfile()
assert(viewModel.loadingStatus.value != null)
assert(!UITestUtil.getValue(viewModel.loadingStatus)!!)
assertEquals(false, viewModel.loadingStatus.value!!)
}
You won't have to use those countdownlatches etc. This approach has literally fixed all of my issues with testing coroutines, hope it helps you as well :)! If not, just let me know.

When unit testing my ViewModel, list always returns empty

I'm trying to learn unit testing, I have my code factored into an MVVM(i) architecture, but when I run my testParseToList() test function, it always comes back with an empty list and I can't figure out why. I fear it may have something to do with the i part of the MVVM(i) and whether or not I'm correctly mocking my viewmodel. I'm starting with my most simple viewmodel in hopes to get a grasp of the concepts before moving onto my more complex ones.
OfflineViewModelUnitTest.kt
#RunWith(JUnit4::class)
class OfflineViewModelUnitTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
var offlineViewModel: OfflineViewModel = OfflineViewModel(OfflineInteractorImpl())
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
DaggerOfflineViewModelComponent.builder()
.offlineInteractorImplModule(OfflineInteractorImplModule())
.build()
.inject(offlineViewModel)
// this.offlineViewModel = OfflineViewModel(OfflineInteractorImpl())
}
#Test
fun testParseToList() {
val test = offlineViewModel.parseTextToList("dried bonito extract,\n" +
" ketchup,\n" +
" millet,\n" +
" corn & wheat protein")
val a = "dried bonito extract"
val b = "ketchup"
val c = "millet"
val d = "corn & wheat protein"
val expectedResult = listOf(a, b, c, d)
assertEquals(expectedResult, test)
}
}
OfflineViewModel.kt
class OfflineViewModel(private val offlineInteractor: OfflineInteractor): ViewModel() {
init {
DaggerOfflineViewModelComponent.builder()
.offlineInteractorImplModule(OfflineInteractorImplModule())
.build()
.inject(this)
}
fun parseTextToList(firebaseVisionTextString: String): MutableList<String> {
Log.d("here it is", firebaseVisionTextString)
return offlineInteractor.parseTextToList(firebaseVisionTextString)
}
fun readCsvFromAssetFolder(inputStream: InputStream): List<String>{
return offlineInteractor.readCsvFromAssetFolder(inputStream)
}
}
OfflineInteractorImpl.kt
class OfflineInteractorImpl: OfflineInteractor {
override fun parseTextToList(firebaseVisionTextString: String): MutableList<String> {
val ingredientsList: MutableList<String> = firebaseVisionTextString.split(",").map { it.trim() }.toMutableList()
return ingredientsList
}
override fun readCsvFromAssetFolder(inputStream: InputStream): List<String> {
val csvLine: ArrayList<String> = ArrayList()
var content: Array<String>?
try
{
val br = BufferedReader(InputStreamReader(inputStream))
for (line in br.lines())
{
content = line.split((",").toRegex()).dropLastWhile{ it.isEmpty() }.toTypedArray()
csvLine.add(content[0].substringBefore(";"))
}
br.close()
}
catch (e: IOException) {
e.printStackTrace()
}
return csvLine
}
}
Test Results
java.lang.AssertionError:
Expected :[dried bonito extract, ketchup, millet, corn & wheat protein]
Actual :[]
Like second said, since you mocked offlineViewModel it is going to return an empty string, unless you define something for it to return using when().
Source: https://github.com/mockito/mockito/wiki/FAQ#what-values-do-mocks-return-by-default

Spock unclear assertion behavior

As I know, one way to verify test result is to write expressions into the then section, that evaluates to boolean.
However lately I experienced a behavior which I don't understand. It seems like when one tries to verify something, that is in a block, then assertion only works with an explicit assert keyword.
Here is the example. I wrote a dummy if statement to have a block, but it's the same with a for loop or any control flow.
def "test fails as expected"() {
when: "result has some value"
def result = "someValue"
then: "result has the expected value"
result == "otherValue"
}
def "test passes, but shouldn't"() {
when: "result has some value"
def result = "someValue"
then: "result has the expected value"
if (true) {
result == "otherValue"
}
}
def "test fails as expected when using assert"() {
when: "result has some value"
def result = "someValue"
then: "result has the expected value"
if (true) {
assert result == "otherValue"
}
}
I find this behavior a bit misleading. Can someone explain why it works that way? Is this a bug or the usage is incorrect?
Following Spock documentation:
The when and then blocks always occur together. They describe a stimulus and the expected response. Whereas when blocks may contain arbitrary code, then blocks are restricted to conditions, exception conditions, interactions, and variable definitions. A feature method may contain multiple pairs of when-then blocks.
This explains why Spocks AST transformer does not see the following then block:
then:
if (true) {
result == "otherValue"
}
as a correct one and it does not transform it to SpockRuntime.verifyCondition() invocation.
If you compile your class (with static compilation enabled for better readability) and check the bytecode you will see something similar to this:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import groovy.lang.GroovyObject;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.spockframework.runtime.ErrorCollector;
import org.spockframework.runtime.SpockRuntime;
import org.spockframework.runtime.ValueRecorder;
import org.spockframework.runtime.model.BlockKind;
import org.spockframework.runtime.model.BlockMetadata;
import org.spockframework.runtime.model.FeatureMetadata;
import org.spockframework.runtime.model.SpecMetadata;
import spock.lang.Specification;
#SpecMetadata(
filename = "OtherSpec.groovy",
line = 4
)
public class OtherSpec extends Specification implements GroovyObject {
public OtherSpec() {
}
public Object test(String result) {
return true ? ScriptBytecodeAdapter.compareEqual(result, "otherValue") : null;
}
#FeatureMetadata(
line = 7,
name = "test fails as expected",
ordinal = 0,
blocks = {#BlockMetadata(
kind = BlockKind.WHEN,
texts = {"result has some value"}
), #BlockMetadata(
kind = BlockKind.THEN,
texts = {"result has the expected value"}
)},
parameterNames = {}
)
public void $spock_feature_0_0() {
ErrorCollector $spock_errorCollector = new ErrorCollector(false);
ValueRecorder $spock_valueRecorder = new ValueRecorder();
Object var10000;
try {
String result = "someValue";
try {
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(12), Integer.valueOf(9), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));
var10000 = null;
} catch (Throwable var13) {
SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "result == \"otherValue\"", Integer.valueOf(12), Integer.valueOf(9), (Object)null, var13);
var10000 = null;
} finally {
;
}
ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
} finally {
$spock_errorCollector.validateCollectedErrors();
var10000 = null;
}
}
#FeatureMetadata(
line = 15,
name = "test passes, but shouldn't",
ordinal = 1,
blocks = {#BlockMetadata(
kind = BlockKind.WHEN,
texts = {"result has some value"}
), #BlockMetadata(
kind = BlockKind.THEN,
texts = {"result has the expected value"}
)},
parameterNames = {}
)
public void $spock_feature_0_1() {
String result = "someValue";
if (true) {
ScriptBytecodeAdapter.compareEqual(result, "otherValue");
}
ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
}
#FeatureMetadata(
line = 25,
name = "test fails as expected when using assert",
ordinal = 2,
blocks = {#BlockMetadata(
kind = BlockKind.WHEN,
texts = {"result has some value"}
), #BlockMetadata(
kind = BlockKind.THEN,
texts = {"result has the expected value"}
)},
parameterNames = {}
)
public void $spock_feature_0_2() {
ErrorCollector $spock_errorCollector = new ErrorCollector(false);
ValueRecorder $spock_valueRecorder = new ValueRecorder();
Object var10000;
try {
String result = "someValue";
DefaultGroovyMethods.println(this, this.test("otherValue"));
var10000 = null;
if (true) {
try {
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));
var10000 = null;
} catch (Throwable var13) {
SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, var13);
var10000 = null;
} finally {
;
}
}
ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
} finally {
$spock_errorCollector.validateCollectedErrors();
var10000 = null;
}
}
}
Now, if we analyze this code, we will find out that the following Spock test case:
def "test fails as expected"() {
when: "result has some value"
def result = "someValue"
then: "result has the expected value"
result == "otherValue"
}
compiles to something like this:
public void $spock_feature_0_0() {
ErrorCollector $spock_errorCollector = new ErrorCollector(false);
ValueRecorder $spock_valueRecorder = new ValueRecorder();
Object var10000;
try {
String result = "someValue";
try {
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(12), Integer.valueOf(9), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));
var10000 = null;
} catch (Throwable var13) {
SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "result == \"otherValue\"", Integer.valueOf(12), Integer.valueOf(9), (Object)null, var13);
var10000 = null;
} finally {
;
}
ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
} finally {
$spock_errorCollector.validateCollectedErrors();
var10000 = null;
}
}
And the test case where you put assertion inside if-statement:
def "test passes, but shouldn't"() {
when: "result has some value"
def result = "someValue"
then: "result has the expected value"
if (true) {
result == "otherValue"
}
}
compiles to something like this:
public void $spock_feature_0_1() {
String result = "someValue";
if (true) {
ScriptBytecodeAdapter.compareEqual(result, "otherValue");
}
ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
}
If you are interested in investigating the source code of this AST transformation you can start by analyzing:
org.spockframework.compiler.SpecRewriter.visitThenBlock() method
org.spockframework.compiler.DeepBlockRewriter.handleImplicitCondition() method
And for the last use case - adding assert to the if-statement block is the explicit instruction for Spock that it has to be transformed to verification condition invocation. That's why you see the bytecode that decompiles to something like this:
public void $spock_feature_0_2() {
ErrorCollector $spock_errorCollector = new ErrorCollector(false);
ValueRecorder $spock_valueRecorder = new ValueRecorder();
Object var10000;
try {
String result = "someValue";
DefaultGroovyMethods.println(this, this.test("otherValue"));
var10000 = null;
if (true) {
try {
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));
var10000 = null;
} catch (Throwable var13) {
SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, var13);
var10000 = null;
} finally {
;
}
}
ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
} finally {
$spock_errorCollector.validateCollectedErrors();
var10000 = null;
}
}
Notice that if (true) { /*...*/ } is still present because AST transformer still ignores transforming it, but the condition:
assert result == "otherValue"
was visited and accepted by the AST transformer, and replaced by:
SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));