Using PowerMock and Mockito in an Android Instrume

2019-06-17 09:54发布

问题:

I”m trying to use PowerMock to mock a class with a static method, but specifically I wish to do this within an Android Instrumentation test. To be clear I wish to run the test on a real Android device or an emulator. I am using Android Studio (1.5.1) and Gradle (1.5.0). To avoid any red herrings I have created a really basic and rather crude ‘hello world’ app. This app simply shows 2 pieces of text, one retrieved from a static method and one from a non-static method. I have written instrumentation tests for both of these ‘text provider’ classes. You can see this app here:

https://github.com/Kai2k/PowerMockAndroidTest.git

When attempting to run the instrumentation tests I get the error which it appears many people trying to achieve this get:

com.android.build.api.transform.TransformException: com.android.builder.packaging.DuplicateFileException: Duplicate files copied in APK mockito-extensions/org.mockito.plugins.MockMaker
File1: /Users/kaiarmer/.gradle/caches/modules-2/files-2.1/com.crittercism.dexmaker/dexmaker-mockito/1.4/70892a94894462c1b35df3c8a77d21b7e843550b/dexmaker-mockito-1.4.jar
File2: /Users/kaiarmer/.gradle/caches/modules-2/files-2.1/org.powermock/powermock-api-mockito/1.6.4/fe12509b7e9e49d25131f4155145748a31e42e40/powermock-api-mockito-1.6.4.jar

My dependencies on my app’s build.gradle file look like this:

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
testCompile 'org.powermock:powermock-api-mockito:1.6.4'
testCompile 'org.powermock:powermock-module-junit4-rule-agent:1.6.4'
testCompile 'org.powermock:powermock-module-junit4-rule:1.6.4'
testCompile 'org.powermock:powermock-module-junit4:1.6.4'

androidTestCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile 'org.powermock:powermock-api-mockito:1.6.4'
androidTestCompile 'com.android.support:support-annotations:23.1.1'
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'

compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
}

You will notice some ‘testCompile’ dependencies on power mock and related libraries (in addition to androidTestCompile). This is because for belt and braces I wanted to see if I could get a junit (non-instrumentation) test (which uses power mock) working also. I was able to achieve this, but not the instrumentation test. Here is my (horrible) code:

Main Activity:

package com.example.android.powermocktest;

import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        }
    });

    TextView textView1 = (TextView)findViewById(R.id.textView1);
    textView1.setText(GetStaticValue.getTheStaticValue());

    TextView textView2 = (TextView)findViewById(R.id.textView2);
    textView2.setText(new GetNonStaticValue().getNonStaticString());
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
        return true;
    }

    return super.onOptionsItemSelected(item);
}
}

Layout files:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout         xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.example.android.powermocktest.MainActivity">

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_main" />

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email" />

</android.support.design.widget.CoordinatorLayout>

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.example.android.powermocktest.MainActivity"
tools:showIn="@layout/activity_main">

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/textView1"
    android:text="Hello World!" />
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/textView1"
    android:id="@+id/textView2"
    android:text="Hello World again!" />
</RelativeLayout>

And the classes to get the static and non-static values:

package com.example.android.powermocktest;

public class GetStaticValue {

private final static String THE_VALUE = "Hi, I'm static";

public static String getTheStaticValue(){
    return THE_VALUE;
}
}

package com.example.android.powermocktest;

public class GetNonStaticValue {

public String getNonStaticString(){
    return "Hi, I'm non-static";
}
}

And finally the test classes. First the instrumentation tests:

package com.example.android.powermocktest;

import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(AndroidJUnit4.class)
@PrepareForTest(GetStaticValue.class)
public class GetStaticValueTest {

    @Test
    public void getStaticValueReturnsValue(){
        PowerMockito.mockStatic(GetStaticValue.class);
        when(GetStaticValue.getTheStaticValue()).thenReturn("Hi, I'm static");
        assertThat(GetStaticValue.getTheStaticValue(), equalTo("Hi, I'm static"));
    }
}

package com.example.android.powermocktest;

import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.when;

@RunWith(AndroidJUnit4.class)
public class GetNonStaticValueTest {

    @Test
    public void getNonStaticValueReturnsNonStaticValue(){
        GetNonStaticValue getNonStaticValue = Mockito.mock(GetNonStaticValue.class);
        when(getNonStaticValue.getNonStaticString()).thenReturn("Hi, I'm non-static");
        assertThat(getNonStaticValue.getNonStaticString(), equalTo("Hi, I'm non-static"));
    }
}

And now the jUnit tests (which work):

package com.example.android.powermocktest;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest(GetStaticValue.class)
public class GetStaticValueTest {

    @Test
    public void getStaticValueReturnsValue() {
        PowerMockito.mockStatic(GetStaticValue.class);
        when(GetStaticValue.getTheStaticValue()).thenReturn("Hi, I'm static");
        assertThat(GetStaticValue.getTheStaticValue(), equalTo("Hi, I'm static"));
    }
}

What I’ve tried so far:

I’ve done some reading around this issue and one solution I’ve found, which I have not tried is to manually crack open the power mock library jars and remove the offending duplicate class. As (at work) we use gradle and don’t work with local jars I’d prefer not to do this.

How to get Powermock to work with Dexmaker

I’ve heard people suggest using ‘exclude’ within the Android section of the gradle build file. This is not suitable as for the instrumentation test to run it is built (by Android Studio / Gradle) as an apk and launched on the device. As such the power mock jars are needed to run the tests.

android studio: gradle dependency error

I’ve tried removing some of the dependencies which you see listed in my build file above. For example I’ve tried removing the ‘org.powermock:powermock-api-mockito’ dependency, but it is needed to use ‘mockStatic’ for example.

It seems that someone was successful when using Maven, by telling Maven to exclude duplicates:

https://groups.google.com/forum/#!searchin/powermock/android/powermock/ndZ2ZliYGCY/Eh226605u2cJ

PowerMock + Mockito + Maven on Android app showing Dex loader error

I’ve tried to see if there is a way of telling Gradle to ignore duplicate classes within dependency jars but so far I have been unsuccessful.

I feel this is worth pursuing as firstly, power mock can be very useful when used sparingly and second, I cannot believe that this issues has not been solved (it’s certainly been encountered before!).

As a final point, I did notice that Google themselves seem to inherently suggest that mocking is only for unit, not instrumentation tests:

http://developer.android.com/training/testing/start/index.html

Any help anyone can offer would be greatly appreciated. Thanks.

回答1:

I was able to workaround this issue with the following. If you see any error message that says Duplicate files copied in APK [filename], add that [filename] to be excluded in the packagingOptions.

android {
    packagingOptions {
        exclude 'mockito-extensions/org.mockito.plugins.MockMaker'
    }
}


回答2:

Do you have this into your project/module gradle?

packagingOptions {
    exclude 'fileNameYouWantToExclude'
}

In this way Androd will put just one file if duplicates are found