Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't ArgumentCaptor matching up properly?

I'm working on an Android app, using AndroidStudio and am hoping someone could tell me why I can't get Mockito to recognize arguments using argumentCaptor.capture() or anyObject().

I'm testing SpanPainter's method applyColor():

package com.olfybsppa.inglesaventurero.utils;

import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;

public class SpanPainter {
  ForegroundColorSpan color;

  public SpanPainter (ForegroundColorSpan color) {
    this.color = color;
  }
  public SpannableString applyColor(SpannableString span) {
    span.setSpan(color, 1, 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    return span;
  }
}

My test is:

package com.olfybsppa.inglesaventurero.utils;

import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class SpanPainterTest {
  @Test
  public void testAppliesColorPerRange () {
    SpannableString mockSpanString = mock(SpannableString.class);
    ForegroundColorSpan mockForegroundColor = mock(ForegroundColorSpan.class);
    SpanPainter painter = new SpanPainter(mockForegroundColor);

    ArgumentCaptor<ForegroundColorSpan> argumentCaptor = ArgumentCaptor.forClass(ForegroundColorSpan.class);
    painter.applyColor(mockSpanString);

    verify(mockSpanString).setSpan(argumentCaptor.capture(), anyInt(), anyInt(), anyInt());
    //verify(mockSpanString).setSpan(anyObject(), anyInt(), anyInt(), anyInt());
  }
}

The results are: (I removed the angle brackets)

Argument(s) are different! Wanted:
spannableString.setSpan(
    Capturing argument,
    any,
    any,
    any
);
Actual invocation has different arguments:
spannableString.setSpan(
    Mock for ForegroundColorSpan, hashCode: 106298691,
    1,
    2,
    17
);

If I remove the commented line and use anyObject(), the results are:

Argument(s) are different! Wanted:
spannableString.setSpan(any,any,any,any);
Actual invocation has different arguments:
spannableString.setSpan(
    Mock for ForegroundColorSpan, hashCode: 106298691,
    1,
    2,
    17
);

It seems to me that at least using anyObject() should work, but it doesn't.

Not referencing my main code anymore, still using Android api objects

Following code gives similar results, 'Arguments are different!Wanted...' 'Capturing argument' vs 'Mock for ForegroundColorSpan, hashCode: xxxx':

package com.olfybsppa.inglesaventurero.utils;

import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class DummyTest {
  @Test
  public void testCaptor() {
    SpannableString helper = mock(SpannableString.class);
    ForegroundColorSpan color = mock(ForegroundColorSpan.class);
    helper.setSpan(color, 1, 2, 17);
    ArgumentCaptor<ForegroundColorSpan> captor = ArgumentCaptor.forClass(ForegroundColorSpan.class);
    verify(helper).setSpan(captor.capture(), anyInt(), anyInt(), anyInt());
  }

}

Gradle settings:

Here are my gradle settings:

From overall build.gradle file

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:1.3.0'
  }
}

allprojects {
  repositories {
    jcenter()
  }
}

From app build.gradle file:

apply plugin: 'com.android.application'

android {
  compileSdkVersion 20
  buildToolsVersion "20.0.0"

  defaultConfig {
    applicationId "com.olfybsppa.inglesadventurero"
    minSdkVersion 15
    targetSdkVersion 15
    versionCode 5
    versionName "5.0"
  }
  buildTypes {
    release {
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
  sourceSets {
    main {
    java.srcDirs = ['src/main/java', 'src/main/java/start', 'src/main/java/adapters', 'src/main/java/pagers', 'src/main/java/com.olfybsppa.inglesadventurero/pagers']
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_7
    targetCompatibility JavaVersion.VERSION_1_7
  }
  testOptions {
    unitTests.returnDefaultValues = true
  }
}

dependencies {
  compile 'com.android.support:support-v4:20.0.+'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'com.google.android.gms:play-services-ads:6.+'
  testCompile 'junit:junit:4.12'
  testCompile 'org.mockito:mockito-core:1.10.19'
}

PowerMockito also doesn't work:

I also tried the following, using PowerMockito:

@RunWith(PowerMockRunner.class)
@PrepareForTest({ SpannableString.class, ForegroundColorSpan.class })
public class DummyTest {
  @Test
  public void testCaptor() {
    SpannableString helper = PowerMockito.mock(SpannableString.class);
    ForegroundColorSpan color = PowerMockito.mock(ForegroundColorSpan.class);

which continues as before; this also doesn't solve the problem.

Adding non-android interface:

This test uses returnDefaultValues = true, does not use Powermockito, and uses Object instead of ForegroundColorSpan. Testing to see if subclassing android method and implementing a non-android interface would work.

public class SpanPainterTest {
  @Test
  public void testCaptor() {
    SpannableStringSubclass helper = mock(SpannableStringSubclass.class);
    Object color = mock(Object.class);
    helper.setSpan(color, 1, 2, 17);
    ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
    verify(helper).setSpan(anyObject(), anyInt(), anyInt(), anyInt());
    //verify(helper).setSpan(captor.capture(), anyInt(), anyInt(), anyInt());
  }
}

Interface:

public interface SpannableStringSuper {
  public void setSpan(Object what, int start, int end, int flags);
}

Subclass:

public class SpannableStringSubclass extends SpannableString implements SpannableStringSuper {
  public SpannableStringSubclass () {
    super("xxx");
  }
}

This results in very similar results as first test. 'any' vs 'Mock for Object'. And 'Capturing argument' vs 'Mock for Object'.

like image 944
flobacca Avatar asked Aug 17 '15 16:08

flobacca


2 Answers

I can't reproduce this. Here's the code I used to try to match your scenario:

public class DummyTest {
  @Test
  public void testCaptor() {
    SpannableString helper = mock(SpannableString.class);
    ForegroundColorSpan color = mock(ForegroundColorSpan.class);
    helper.setSpan(color, 1, 2, 17);
    ArgumentCaptor<ForegroundColorSpan> captor = ArgumentCaptor.forClass(ForegroundColorSpan.class);
    verify(helper).setSpan(captor.capture(), anyInt(), anyInt(), anyInt());
  }
}

This passes just fine for me. I'm using:

  • Maven
  • Android 4.1.1.4
  • JUnit 4.11
  • Mockito 1.10.19

Clearly, the fact that mine works and yours doesn't means there's some issue with something outside the class; it could either be a dependency issue or a mock settings configuration issue.

Is it possible you're somehow using two different versions of ForegroundColorSpan? Are you setting Mockito settings outside of the tests you've shown me somewhere?

You could try would be swapping them both out separately and seeing if they work. For example, try this:

public class DummyTest {
  @Test
  public void testCaptor() {
    TestSpannable helper = mock(TestSpannable.class);
    ForegroundColorSpan color = mock(ForegroundColorSpan.class);
    helper.setSpan(color, 1, 2, 17);
    ArgumentCaptor<ForegroundColorSpan> captor = ArgumentCaptor.forClass(ForegroundColorSpan.class);
    verify(helper).setSpan(captor.capture(), anyInt(), anyInt(), anyInt());
  }

  public static interface TestSpannable {
    public void setString(Object what, int start, int end, int flags);
  }
}

Then, try something similar using a SpannableString type and a different Object where you're currently using ForegroundColorSpan.


I tried running DummyTest again, with RETURNS_DEFAULTS based on your answer - but doing it inside the test class and not in the build settings, like so:

public class DummyTest {
  @Test
  public void testCaptor() {
    SpannableString helper = mock(SpannableString.class, RETURNS_DEFAULTS);
    ForegroundColorSpan color = mock(ForegroundColorSpan.class, RETURNS_DEFAULTS);
    helper.setSpan(color, 1, 2, 17);
    ArgumentCaptor<ForegroundColorSpan> captor = ArgumentCaptor.forClass(ForegroundColorSpan.class);
    verify(helper).setSpan(captor.capture(), anyInt(), anyInt(), anyInt());
  }
}

This also works and doesn't throw your error. Therefore it has something to do with the gradle build setting itself, and not RETURNS_DEFAULTS on it's face.

like image 54
durron597 Avatar answered Oct 01 '22 01:10

durron597


I think that having unitTest.returnDefaultValues = true in the project gradle.build file results in default mock objects instead of plain mock Android objects. I think that Mockito does not allow these default mock objects to be used as true mock objects. I think this because method verification on these default mock objects never passed in my tests.

defaultObject = mock(SomeAndroidClass.class)
verify(defaultObject).method(argumentCaptor.capture()) 

The above will not pass. The arguments never matched.

I know this answer is not completely substantiated by the documentation, but it is my understanding as of now. I base this on the tests I ran, see question text and also from this link in the "Method... not mocked" section.

It does seem that mock Android objects can be used as ArgumentCaptor types. These act more like "doubles", in that method verification is not performed on these default mock objects. I think this is true because @durron597's DummyTest with the TestSpannable interface passed.

like image 28
flobacca Avatar answered Sep 30 '22 23:09

flobacca