Menggunakan Mockito untuk menguji kelas abstrak

213

Saya ingin menguji kelas abstrak. Tentu, saya bisa secara manual menulis tiruan yang mewarisi dari kelas.

Bisakah saya melakukan ini menggunakan kerangka kerja mengejek (saya menggunakan Mockito) alih-alih membuat kerajinan tangan tiruan saya? Bagaimana?

ripper234
sumber
2
Pada Mockito 1.10.12 , Mockito mendukung mata-mata / mengejek kelas abstrak secara langsung:SomeAbstract spy = spy(SomeAbstract.class);
pesche
6
Pada Mockito 2.7.14, Anda juga dapat mengejek kelas abstrak yang membutuhkan argumen konstruktor viamock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Jawaban:

315

Saran berikut memungkinkan Anda menguji kelas abstrak tanpa membuat subclass "nyata" - Mock adalah subclass.

gunakan Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), lalu tiru metode abstrak apa saja yang dipanggil.

Contoh:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

Catatan: Keindahan solusi ini adalah Anda tidak harus menerapkan metode abstrak, asalkan tidak pernah diminta.

Menurut pendapat jujur ​​saya, ini lebih rapi daripada menggunakan mata-mata, karena mata-mata membutuhkan contoh, yang berarti Anda harus membuat subkelas kelas abstrak Anda yang tidak dapat dipakai.

Morten Lauritsen Khodabocus
sumber
14
Seperti dicatat di bawah, ini tidak berfungsi ketika kelas abstrak memanggil metode abstrak untuk diuji, yang sering terjadi.
Richard Nichols
11
Ini sebenarnya berfungsi ketika kelas abstrak memanggil metode abstrak. Cukup gunakan sintaks doReturn atau doNothing alih-alih Mockito.when untuk mematikan metode abstrak, dan jika Anda membatalkan panggilan konkret, pastikan mematikan panggilan abstrak terlebih dahulu.
Gonen I
2
Bagaimana saya bisa menyuntikkan dependensi dalam objek semacam ini (kelas abstrak mengejek memanggil metode nyata)?
Samuel
2
Ini berperilaku dengan cara yang tidak terduga jika kelas tersebut memiliki inisialisasi instance. Mockito melewatkan initializers untuk mock, yang berarti variabel instan yang diinisialisasi inline akan tiba-tiba nol, yang dapat menyebabkan NPE.
digitalbath
1
Bagaimana jika konstruktor kelas abstrak mengambil satu atau lebih parameter?
SD
68

Jika Anda hanya perlu menguji beberapa metode konkret tanpa menyentuh abstrak apa pun, Anda dapat menggunakan CALLS_REAL_METHODS(lihat jawaban Morten ), tetapi jika metode konkret yang diuji memanggil beberapa abstrak, atau metode antarmuka yang tidak diterapkan, ini tidak akan berfungsi - Mockito akan mengeluh "Tidak dapat memanggil metode nyata di antarmuka java."

(Ya, ini desain yang buruk, tetapi beberapa kerangka kerja, misalnya Tapestry 4, semacam memaksakannya pada Anda.)

Solusinya adalah membalikkan pendekatan ini - gunakan perilaku mengejek biasa (yaitu, semuanya diejek / di-stub) dan gunakan doCallRealMethod()untuk secara eksplisit memanggil metode konkret yang sedang diuji. Misalnya

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Diperbarui untuk menambahkan:

Untuk metode non-void, Anda harus menggunakan thenCallRealMethod(), misalnya:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

Kalau tidak, Mockito akan mengeluh "Kegagalan yang belum selesai terdeteksi."

David Moles
sumber
9
Ini akan berfungsi dalam beberapa kasus, namun Mockito tidak memanggil konstruktor dari kelas abstrak yang mendasari dengan metode ini. Ini dapat menyebabkan "metode nyata" gagal karena skenario tak terduga dibuat. Dengan demikian, metode ini juga tidak akan berfungsi dalam semua kasus.
Richard Nichols
3
Yap, Anda tidak bisa mengandalkan keadaan objek sama sekali, hanya kode dalam metode yang dipanggil.
David Moles
Oh, jadi metode objek terpisah dari negara, bagus.
haelix
17

Anda dapat mencapai ini dengan menggunakan mata-mata (gunakan versi terbaru Mockito 1.8+).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));
Richard Nichols
sumber
14

Kerangka kerja mengejek dirancang untuk membuatnya lebih mudah untuk mengejek dependensi kelas yang Anda uji. Saat Anda menggunakan kerangka kerja mengejek untuk mengejek kelas, sebagian besar kerangka kerja secara dinamis membuat subkelas, dan mengganti implementasi metode dengan kode untuk mendeteksi kapan metode dipanggil dan mengembalikan nilai palsu.

Saat menguji kelas abstrak, Anda ingin menjalankan metode non-abstrak dari Subject Under Test (SUT), jadi kerangka kerja mengejek bukanlah yang Anda inginkan.

Bagian dari kebingungan adalah bahwa jawaban untuk pertanyaan yang Anda tautkan dengan kerajinan tangan merupakan tiruan yang meluas dari kelas abstrak Anda. Saya tidak akan menyebut kelas seperti itu pura-pura. Mock adalah kelas yang digunakan sebagai pengganti ketergantungan, diprogram dengan harapan, dan dapat ditanyakan untuk melihat apakah harapan tersebut terpenuhi.

Sebagai gantinya, saya sarankan mendefinisikan subclass non-abstrak dari kelas abstrak Anda dalam pengujian Anda. Jika itu menghasilkan terlalu banyak kode, maka itu mungkin pertanda bahwa kelas Anda sulit diperluas.

Solusi alternatif adalah membuat test case Anda sendiri abstrak, dengan metode abstrak untuk membuat SUT (dengan kata lain, case test akan menggunakan pola desain Metode Templat ).

NamshubWriter
sumber
8

Coba gunakan jawaban khusus.

Sebagai contoh:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Ini akan mengembalikan tiruan untuk metode abstrak dan akan memanggil metode nyata untuk metode konkret.


sumber
5

Apa yang benar-benar membuat saya merasa tidak enak tentang mengejek kelas abstrak adalah fakta, bahwa konstruktor default YourAbstractClass () tidak dipanggil (missing super () di mock) atau tampaknya tidak ada cara apa pun di Mockito untuk menginisialisasi default properti tiruan (misalnya properti Daftar dengan ArrayList kosong atau LinkedList).

Kelas abstrak saya (pada dasarnya kode sumber kelas dihasilkan) TIDAK memberikan injeksi setter dependensi untuk elemen daftar, atau konstruktor di mana ia menginisialisasi elemen daftar (yang saya coba tambahkan secara manual).

Hanya atribut kelas yang menggunakan inisialisasi default: Daftar pribadi dep1 = new ArrayList; Daftar pribadi dep2 = ArrayList baru

Jadi tidak ada cara untuk mengejek kelas abstrak tanpa menggunakan implementasi objek nyata (misalnya definisi kelas dalam dalam kelas unit test, metode abstrak utama) dan memata-matai objek nyata (yang melakukan inisialisasi bidang yang tepat).

Sayang sekali bahwa hanya PowerMock akan membantu di sini lebih lanjut.

Thomas Heiss
sumber
2

Dengan asumsi kelas tes Anda berada dalam paket yang sama (di bawah sumber root berbeda) seperti kelas Anda yang sedang diuji, Anda bisa membuat tiruan:

YourClass yourObject = mock(YourClass.class);

dan panggil metode yang ingin Anda uji seperti halnya metode lainnya.

Anda perlu memberikan harapan untuk setiap metode yang dipanggil dengan harapan pada metode konkret yang memanggil metode super - tidak yakin bagaimana Anda akan melakukannya dengan Mockito, tapi saya percaya itu mungkin dengan EasyMock.

Semua ini dilakukan adalah menciptakan contoh konkret YouClassdan menyelamatkan Anda dari upaya menyediakan implementasi kosong dari setiap metode abstrak.

Selain itu, saya sering merasa berguna untuk mengimplementasikan kelas abstrak dalam pengujian saya, di mana ia berfungsi sebagai contoh implementasi yang saya uji melalui antarmuka publiknya, meskipun hal ini bergantung pada fungsionalitas yang disediakan oleh kelas abstrak.

Nick Holt
sumber
3
Tetapi menggunakan tiruan tidak akan menguji metode konkret dari YourClass, atau apakah saya salah? Ini bukan yang saya cari.
ripper234
1
Itu benar, di atas tidak akan berfungsi jika Anda ingin memanggil metode konkret pada kelas abstrak.
Richard Nichols
Maaf, saya akan mengedit sedikit tentang harapan, yang diperlukan untuk setiap metode yang Anda panggil bukan hanya yang abstrak.
Nick Holt
tetapi kemudian Anda masih menguji tiruan Anda, bukan metode konkret.
Jonatan Cloutier
2

Anda dapat memperluas kelas abstrak dengan kelas anonim dalam pengujian Anda. Misalnya (menggunakan Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.
DwB
sumber
2

Mockito memungkinkan mengejek kelas abstrak dengan menggunakan @Mockanotasi:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

Kerugiannya adalah tidak dapat digunakan jika Anda membutuhkan parameter konstruktor.

Jorge Pastor
sumber
0

Anda dapat membuat instance kelas anonim, menyuntikkan tiruan Anda dan kemudian menguji kelas itu.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Perlu diingat bahwa visibilitas harus protecteduntuk properti myDependencyServicekelas abstrak ClassUnderTest.

Samuel
sumber
0

PowerMock Whitebox.invokeMethod(..)dapat berguna dalam kasus ini.

Smart Coder
sumber