Gunakan Mockito untuk mengejek beberapa metode tetapi tidak yang lain

402

Apakah ada cara, menggunakan Mockito, untuk mengejek beberapa metode di kelas, tetapi tidak yang lain?

Sebagai contoh, dalam kelas ini (diakui dibuat-buat) Stocksaya ingin mengejek getPrice()dan getQuantity()mengembalikan nilai (seperti yang ditunjukkan dalam cuplikan tes di bawah) tetapi saya ingin getValue()melakukan penggandaan seperti yang dikodekan di Stockkelas.

public class Stock {
  private final double price;
  private final int quantity;

  Stock(double price, int quantity) {
    this.price = price;
    this.quantity = quantity;
  }

  public double getPrice() {
    return price;
  }

  public int getQuantity() {
    return quantity;
  }
  public double getValue() {
    return getPrice() * getQuantity();
  }

  @Test
  public void getValueTest() {
    Stock stock = mock(Stock.class);
    when(stock.getPrice()).thenReturn(100.00);
    when(stock.getQuantity()).thenReturn(200);
    double value = stock.getValue();
    // Unfortunately the following assert fails, because the mock Stock getValue() method does not perform the Stock.getValue() calculation code.
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}
Victor Grazi
sumber
4
Mengapa Anda ingin melakukan itu? Anda harus menguji kelas (dalam hal ini, seharusnya tidak ada mengejek sama sekali) atau Anda harus mengejeknya saat menguji kelas yang berbeda (dalam hal ini, tidak ada fungsi). Mengapa Anda melakukan tiruan parsial?
weltraumpirat
3
Ok, ini adalah contoh kecil dari hal yang nyata. Pada kenyataannya, saya mencoba untuk menghindari panggilan ke database, dengan mengirimkan nilai-nilai yang dibuat, tetapi saya ingin memverifikasi bahwa metode lain bekerja dengan benar dengan nilai-nilai yang dibuat. Apakah ada cara yang lebih baik untuk melakukan ini?
Victor Grazi
5
Tentu saja: Pindahkan panggilan basis data Anda ke kelas yang terpisah (logika domain dan akses basis data tidak boleh berada dalam kelas yang sama; keduanya adalah dua masalah yang berbeda), ekstrak antarmuka-nya, gunakan antarmuka itu untuk terhubung dari kelas logika domain, dan tiru hanya antarmuka selama pengujian.
weltraumpirat
1
Saya sepenuhnya setuju, sulit untuk menjelaskan keseluruhan gambar tanpa mengunggah sekumpulan kode di sini, termasuk perpustakaan pihak ketiga.
Victor Grazi
1
Anda mungkin bisa. Namun, itu tidak akan menjadi "cara yang lebih baik untuk melakukannya": Kode basis data Anda adalah detail implementasi yang ingin Anda sembunyikan dari aplikasi Anda yang lain, bahkan mungkin pindah ke paket lain. Anda tidak ingin mengkompilasi ulang logika domain Anda setiap kali Anda mengubah pernyataan sekuel, bukan?
weltraumpirat

Jawaban:

644

Untuk langsung menjawab pertanyaan Anda, ya, Anda dapat mengejek beberapa metode tanpa mengejek orang lain. Ini disebut tiruan parsial . Lihat dokumentasi Mockito pada tiruan parsial untuk informasi lebih lanjut.

Sebagai contoh Anda, Anda dapat melakukan sesuatu seperti berikut ini, dalam pengujian Anda:

Stock stock = mock(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
when(stock.getValue()).thenCallRealMethod();  // Real implementation

Dalam hal ini, setiap implementasi metode diejek, kecuali ditentukan thenCallRealMethod()dalam when(..)klausa.

Ada juga kemungkinan sebaliknya dengan mata - mata bukannya tiruan :

Stock stock = spy(Stock.class);
when(stock.getPrice()).thenReturn(100.00);    // Mock implementation
when(stock.getQuantity()).thenReturn(200);    // Mock implementation
// All other method call will use the real implementations

Dalam hal itu, semua implementasi metode adalah yang asli, kecuali jika Anda telah mendefinisikan perilaku yang dipermainkan when(..).

Ada satu perangkap penting ketika Anda menggunakan when(Object)dengan mata-mata seperti pada contoh sebelumnya. Metode nyata akan dipanggil (karena stock.getPrice()dievaluasi sebelum when(..)saat runtime). Ini bisa menjadi masalah jika metode Anda mengandung logika yang tidak boleh dipanggil. Anda dapat menulis contoh sebelumnya seperti ini:

Stock stock = spy(Stock.class);
doReturn(100.00).when(stock).getPrice();    // Mock implementation
doReturn(200).when(stock).getQuantity();    // Mock implementation
// All other method call will use the real implementations

Kemungkinan lain mungkin untuk digunakan org.mockito.Mockito.CALLS_REAL_METHODS, seperti:

Stock MOCK_STOCK = Mockito.mock( Stock.class, CALLS_REAL_METHODS );

Delegasi ini membatalkan panggilan untuk implementasi nyata.


Namun, dengan contoh Anda, saya percaya itu masih akan gagal, karena implementasi getValue()bergantung pada quantitydan price, bukan getQuantity()dan getPrice(), yang merupakan apa yang telah Anda cemooh.

Kemungkinan lain adalah untuk menghindari cemoohan sama sekali:

@Test
public void getValueTest() {
    Stock stock = new Stock(100.00, 200);
    double value = stock.getValue();
    assertEquals("Stock value not correct", 100.00*200, value, .00001);
}
Jon Newmuis
sumber
21
Saya pikir jawaban ini salah. Anda perlu SPY instance objek, bukan untuk MENGEJUT kelas.
GaRRaPeTa
2
@ GaRRaPeTa Saya akan mengatakan memata-matai dan mengejek adalah alternatif yang masuk akal. Sulit untuk mengatakan mana yang terbaik untuk kasus ini, karena OP menyatakan bahwa ini adalah contoh yang disederhanakan.
Jon Newmuis
1
Bukankah itu "Spy", bukan "Mock" karena cabe mengejek parsial disediakan oleh "Spy" dengan cara yang lebih baik.
Tarun Sapra
2
Stock stock = spy(Stock.class);Ini tampaknya salah, spymetode tampaknya hanya menerima objek bukan kelas.
Paramvir Singh Karwal
4
+1 untuk menunjukkan perbedaan antara doReturn(retval).when(spyObj).methodName(args)danwhen(spyObj.methodName(args)).thenReturn(retval)
Captain_Obvious
140

Mengejek sebagian kelas juga didukung melalui Spy di mockito

List list = new LinkedList();
List spy = spy(list);

//optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);

//using the spy calls real methods
spy.add("one");
spy.add("two");

//size() method was stubbed - 100 is printed
System.out.println(spy.size());

Periksa 1.10.19dan 2.7.22dokumentasi untuk penjelasan terperinci.

Sudarshan
sumber
37

Menurut dokumen :

Foo mock = mock(Foo.class, CALLS_REAL_METHODS);

// this calls the real implementation of Foo.getSomething()
value = mock.getSomething();

when(mock.getSomething()).thenReturn(fakeValue);

// now fakeValue is returned
value = mock.getSomething();
ema
sumber
2
Terima kasih telah menunjukkan cara menyiapkan tiruan di mana implementasi sebenarnya dipanggil untuk semua metode kecuali beberapa yang saya perlu kontrol dari tes.
bigh_29
class NaughtyLinkedList extends LinkedList { public int size() { throw new RuntimeException("don't call me");} } @Test public void partialMockNaughtLinkedList(){ List mock = mock(NaughtyLinkedList.class, CALLS_REAL_METHODS); mock.add(new Object()); // this calls the real function when(mock.size()).thenReturn(2); // For whatever reason, this lines throws the RuntimeException. assertEquals(2,mock.size()); }Ini tidak berhasil. Apa pun alasannya, ketika "kapan" dijalankan, sebenarnya mengeksekusi metode yang seharusnya diejek. Kode:
Lance Kind
3
Masalahnya adalah "kapan." "Kapan" sebenarnya akan mengeksekusi hal yang Anda ingin tiruan sebagian. Untuk menghindari ini ada alternatif: doReturn (). Lihat doReturn () di docs.mockito.googlecode.com/hg/1.9.5/org/mockito/…
Lance Kind
18

Apa yang Anda inginkan org.mockito.Mockito.CALLS_REAL_METHODSsesuai dengan dokumen:

/**
 * Optional <code>Answer</code> to be used with {@link Mockito#mock(Class, Answer)}
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations.
 * <p>
 * This implementation can be helpful when working with legacy code.
 * When this implementation is used, unstubbed methods will delegate to the real implementation.
 * This is a way to create a partial mock object that calls real methods by default.
 * <p>
 * As usual you are going to read <b>the partial mock warning</b>:
 * Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy objects.
 * How does partial mock fit into this paradigm? Well, it just doesn't... 
 * Partial mock usually means that the complexity has been moved to a different method on the same object.
 * In most cases, this is not the way you want to design your application.
 * <p>
 * However, there are rare cases when partial mocks come handy: 
 * dealing with code you cannot change easily (3rd party interfaces, interim refactoring of legacy code etc.)
 * However, I wouldn't use partial mocks for new, test-driven & well-designed code.
 * <p>
 * Example:
 * <pre class="code"><code class="java">
 * Foo mock = mock(Foo.class, CALLS_REAL_METHODS);
 *
 * // this calls the real implementation of Foo.getSomething()
 * value = mock.getSomething();
 *
 * when(mock.getSomething()).thenReturn(fakeValue);
 *
 * // now fakeValue is returned
 * value = mock.getSomething();
 * </code></pre>
 */

Dengan demikian kode Anda akan terlihat seperti:

import org.junit.Test;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

public class StockTest {

    public class Stock {
        private final double price;
        private final int quantity;

        Stock(double price, int quantity) {
            this.price = price;
            this.quantity = quantity;
        }

        public double getPrice() {
            return price;
        }

        public int getQuantity() {
            return quantity;
        }

        public double getValue() {
            return getPrice() * getQuantity();
        }
    }

    @Test
    public void getValueTest() {
        Stock stock = mock(Stock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS));
        when(stock.getPrice()).thenReturn(100.00);
        when(stock.getQuantity()).thenReturn(200);
        double value = stock.getValue();

        assertEquals("Stock value not correct", 100.00 * 200, value, .00001);
    }
}

Panggilan ke Stock stock = mock(Stock.class);panggilan org.mockito.Mockito.mock(Class<T>)yang terlihat seperti ini:

 public static <T> T mock(Class<T> classToMock) {
    return mock(classToMock, withSettings().defaultAnswer(RETURNS_DEFAULTS));
}

Dokumen nilai RETURNS_DEFAULTSmemberi tahu:

/**
 * The default <code>Answer</code> of every mock <b>if</b> the mock was not stubbed.
 * Typically it just returns some empty value. 
 * <p>
 * {@link Answer} can be used to define the return values of unstubbed invocations. 
 * <p>
 * This implementation first tries the global configuration. 
 * If there is no global configuration then it uses {@link ReturnsEmptyValues} (returns zeros, empty collections, nulls, etc.)
 */
pria itu
sumber
1
Terlihat dengan baik ... tapi bisakah saya bertanya mengapa Anda menggunakan withSettings()...seperti itu? Tampaknya org.mockito.internal.stubbing.answers.CallsRealMethods()(misalnya) dapat melakukan pekerjaan ... dan javadoc untuk kelas ini secara khusus mengatakan itu untuk digunakan untuk mengolok-olok sebagian ...
mike rodent
3
Juga ... bukankah ini akan menjadi masalah yang ditemui oleh jawaban lain di sini: yaitu thenReturnbenar-benar akan menjalankan metode (yang mungkin menyebabkan masalah, meskipun tidak dalam contoh ini), dan lebih doReturndisukai dalam kasus seperti itu ...?
mike rodent
4

Mengejek sebagian menggunakan metode mata-mata Mockito bisa menjadi solusi untuk masalah Anda, seperti yang sudah dinyatakan dalam jawaban di atas. Untuk tingkat tertentu saya setuju bahwa, untuk kasus penggunaan konkret Anda, mungkin lebih tepat untuk mengejek pencarian DB. Dari pengalaman saya ini tidak selalu mungkin - setidaknya tidak tanpa solusi lain - yang saya anggap sangat rumit atau setidaknya rapuh. Perhatikan, mengejek sebagian itu tidak berfungsi dengan versi sekutu Mockito. Anda telah menggunakan setidaknya 1.8.0.

Saya hanya akan menulis komentar sederhana untuk pertanyaan asli alih-alih memposting jawaban ini, tetapi StackOverflow tidak mengizinkan ini.

Hanya satu hal lagi: Saya benar-benar tidak dapat memahami bahwa beberapa kali pertanyaan diajukan di sini mendapat komentar dengan "Mengapa Anda ingin melakukan ini" tanpa setidaknya mencoba memahami masalahnya. Terutama ketika datang kemudian perlu untuk mengejek parsial benar-benar ada banyak kasus penggunaan yang bisa saya bayangkan di mana itu akan berguna. Itu sebabnya orang-orang dari Mockito menyediakan fungsi itu. Fitur ini tentu saja tidak boleh digunakan secara berlebihan. Tetapi ketika kita berbicara tentang pengaturan kasus uji yang tidak dapat dibuat dengan cara yang sangat rumit, mata-mata harus digunakan.

kflGalore
sumber
2
Saya merasa jawaban ini sebagian merupakan pendapat. Silakan pertimbangkan untuk mengedit.
soundslikeodd
2
Terpilih untuk menghibur anggota baru dalam keluarga. Tidak perlu mendapatkan zona in -ve ini, tidak ada yang salah secara teknis di sana atau bahasa / nada yang salah. Berbaik hati pada anggota baru. Terima kasih.
Saurabh Patil