Perbedaan Antara Stub, Mock, dan Spy dalam Kerangka Spock

1. Ikhtisar

Dalam tutorial ini, kami akan membahas perbedaan antara Mock , Stub , dan Spy dalam kerangka Spock . Kami akan menggambarkan apa yang ditawarkan kerangka kerja dalam kaitannya dengan pengujian berbasis interaksi.

Spock adalah kerangka pengujian untuk Java dan Groovy yang membantu mengotomatiskan proses pengujian manual aplikasi perangkat lunak. Ini memperkenalkan tiruan, rintisan, dan mata-matanya sendiri, dan dilengkapi dengan kemampuan bawaan untuk pengujian yang biasanya membutuhkan pustaka tambahan.

Pertama, kami akan mengilustrasikan kapan kami harus menggunakan stub. Kemudian, kita akan melalui ejekan. Pada akhirnya, kami akan menjelaskan Spy yang baru saja diperkenalkan .

2. Ketergantungan Maven

Sebelum kita mulai, mari tambahkan dependensi Maven kita:

 org.spockframework spock-core 1.3-RC1-groovy-2.5 test   org.codehaus.groovy groovy-all 2.4.7 test 

Perhatikan bahwa kita memerlukan Spock versi 1.3-RC1-groovy-2.5 . Spy akan diperkenalkan di versi stabil Spock Framework berikutnya. Saat ini Spy tersedia dalam kandidat rilis pertama untuk versi 1.3.

Untuk rekap struktur dasar uji Spock, lihat artikel pengantar kami tentang pengujian dengan Groovy dan Spock.

3. Pengujian Berbasis Interaksi

Pengujian berbasis interaksi adalah teknik yang membantu kita menguji perilaku objek - khususnya, bagaimana mereka berinteraksi satu sama lain. Untuk ini, kita dapat menggunakan implementasi dummy yang disebut tiruan dan stub.

Tentu saja, kita tentu bisa dengan mudah menulis implementasi tiruan dan rintisan kita sendiri. Masalahnya muncul ketika jumlah kode produksi kami bertambah. Menulis dan memelihara kode ini dengan tangan menjadi sulit. Inilah sebabnya kami menggunakan kerangka kerja tiruan, yang memberikan cara ringkas untuk menjelaskan secara singkat interaksi yang diharapkan. Spock memiliki dukungan bawaan untuk mengejek, mematikan, dan memata-matai.

Seperti kebanyakan pustaka Java, Spock menggunakan proxy dinamis JDK untuk antarmuka tiruan dan proxy Byte Buddy atau cglib untuk kelas tiruan. Ini membuat implementasi tiruan pada waktu proses.

Java sudah memiliki banyak pustaka yang berbeda dan matang untuk kelas dan antarmuka tiruan. Meskipun masing-masing dapat digunakan di Spock , masih ada satu alasan utama mengapa kita harus menggunakan spock, stub, dan spies. Dengan memperkenalkan semua ini ke Spock, kami dapat memanfaatkan semua kemampuan Groovy untuk membuat pengujian kami lebih mudah dibaca, lebih mudah ditulis, dan pastinya lebih menyenangkan!

4. Panggilan Metode Stubbing

Terkadang, dalam pengujian unit, kita perlu memberikan perilaku tiruan kelas . Ini mungkin klien untuk layanan eksternal, atau kelas yang menyediakan akses ke database. Teknik ini dikenal dengan istilah stubbing.

Stub adalah pengganti yang dapat dikontrol dari dependensi kelas yang ada dalam kode yang kami uji. Ini berguna untuk membuat panggilan metode yang merespons dengan cara tertentu. Saat kami menggunakan rintisan, kami tidak peduli berapa kali metode akan dipanggil. Sebagai gantinya, kami hanya ingin mengatakan: kembalikan nilai ini saat dipanggil dengan data ini.

Mari beralih ke kode contoh dengan logika bisnis.

4.1. Kode Sedang Diuji

Mari buat kelas model yang disebut Item :

public class Item { private final String id; private final String name; // standard constructor, getters, equals }

Kita perlu mengganti metode sama dengan (Object other) agar pernyataan kita berfungsi. Spock akan menggunakan sama dengan selama pernyataan ketika kita menggunakan tanda sama dengan ganda (==):

new Item('1', 'name') == new Item('1', 'name')

Sekarang, mari buat antarmuka ItemProvider dengan satu metode:

public interface ItemProvider { List getItems(List itemIds); }

Kami juga membutuhkan kelas yang akan diuji. Kami akan menambahkan ItemProvider sebagai dependensi di ItemService:

public class ItemService { private final ItemProvider itemProvider; public ItemService(ItemProvider itemProvider) { this.itemProvider = itemProvider; } List getAllItemsSortedByName(List itemIds) { List items = itemProvider.getItems(itemIds); return items.stream() .sorted(Comparator.comparing(Item::getName)) .collect(Collectors.toList()); } }

Kami ingin kode kami bergantung pada abstraksi, bukan implementasi tertentu. Itu sebabnya kami menggunakan antarmuka. Ini dapat memiliki banyak implementasi yang berbeda. Misalnya, kita bisa membaca item dari file, membuat klien HTTP ke layanan eksternal, atau membaca data dari database.

Dalam kode ini, kita perlu menghentikan ketergantungan eksternal, karena kita hanya ingin menguji logika kita yang terdapat dalam metode getAllItemsSortedByName .

4.2. Menggunakan Objek Stub dalam Kode yang Sedang Diuji

Mari kita inisialisasi objek ItemService dalam metode setup () menggunakan Stub untuk dependensi ItemProvider :

ItemProvider itemProvider ItemService itemService def setup() { itemProvider = Stub(ItemProvider) itemService = new ItemService(itemProvider) }

Sekarang, mari membuat itemProvider mengembalikan daftar item pada setiap pemanggilan dengan argumen spesifik :

itemProvider.getItems(['offer-id', 'offer-id-2']) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

Kami menggunakan >> operan untuk menghentikan metode. The getItems metode akan selalu mengembalikan daftar dua item saat dipanggil dengan [ 'menawarkan-id', 'menawarkan-id-2'] daftar. [] adalah cara pintas Groovy untuk membuat daftar.

Berikut seluruh metode pengujian:

def 'should return items sorted by name'() { given: def ids = ['offer-id', 'offer-id-2'] itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')] when: List items = itemService.getAllItemsSortedByName(ids) then: items.collect { it.name } == ['Aname', 'Zname'] }

Ada banyak lagi kemampuan stubbing yang dapat kita gunakan, seperti: menggunakan batasan pencocokan argumen, menggunakan urutan nilai dalam rintisan, mendefinisikan perilaku yang berbeda dalam kondisi tertentu, dan respons metode rantai.

5. Metode Kelas Mengejek

Sekarang, mari kita bicara tentang mengejek kelas atau antarmuka di Spock.

Terkadang, kami ingin mengetahui apakah beberapa metode dari objek dependen dipanggil dengan argumen yang ditentukan . Kami ingin fokus pada perilaku objek dan mengeksplorasi bagaimana mereka berinteraksi dengan melihat pemanggilan metode.Mocking adalah deskripsi interaksi wajib antara objek di kelas pengujian.

Kami akan menguji interaksi dalam kode contoh yang telah kami jelaskan di bawah ini.

5.1. Kode dengan Interaksi

Untuk contoh sederhana, kami akan menyimpan item di database. Setelah sukses, kami ingin menerbitkan acara di broker pesan tentang item baru di sistem kami.

Contoh broker pesan adalah RabbitMQ atau Kafka , jadi secara umum, kami hanya akan menjelaskan kontrak kami:

public interface EventPublisher { void publish(String addedOfferId); }

Metode pengujian kami akan menyimpan item yang tidak kosong di database dan kemudian mempublikasikan acara tersebut. Menyimpan item dalam database tidak relevan dalam contoh kami, jadi kami hanya akan memberi komentar:

void saveItems(List itemIds) { List notEmptyOfferIds = itemIds.stream() .filter(itemId -> !itemId.isEmpty()) .collect(Collectors.toList()); // save in database notEmptyOfferIds.forEach(eventPublisher::publish); }

5.2. Memverifikasi Interaksi dengan Objek yang Dicemooh

Sekarang, mari kita uji interaksi dalam kode kita.

Pertama, kita perlu mengejek EventPublisher dalam metode setup () kita . Jadi pada dasarnya, kami membuat bidang contoh baru dan memalsukannya dengan menggunakan fungsi Mock (Kelas) :

class ItemServiceTest extends Specification { ItemProvider itemProvider ItemService itemService EventPublisher eventPublisher def setup() { itemProvider = Stub(ItemProvider) eventPublisher = Mock(EventPublisher) itemService = new ItemService(itemProvider, eventPublisher) }

Sekarang, kita bisa menulis metode pengujian kita. Kami akan meneruskan 3 String: ”, 'a', 'b' dan kami berharap eventPublisher kami akan menerbitkan 2 acara dengan String 'a' dan 'b':

def 'should publish events about new non-empty saved offers'() { given: def offerIds = ['', 'a', 'b'] when: itemService.saveItems(offerIds) then: 1 * eventPublisher.publish('a') 1 * eventPublisher.publish('b') }

Mari kita lihat lebih dekat pernyataan kita di bagian akhir lalu :

1 * eventPublisher.publish('a')

Kami berharap itemService akan memanggil eventPublisher.publish (String) dengan 'a' sebagai argumennya.

Dalam stubbing, kita telah membicarakan tentang batasan argumen. Aturan yang sama berlaku untuk ejekan. Kita dapat memverifikasi bahwa eventPublisher.publish (String) dipanggil dua kali dengan argumen non-null dan non-empty:

2 * eventPublisher.publish({ it != null && !it.isEmpty() })

5.3. Menggabungkan Mocking dan Stubbing

In Spock, a Mock may behave the same as a Stub. So we can say to mocked objects that, for a given method call, it should return the given data.

Let's override an ItemProvider with Mock(Class) and create a new ItemService:

given: itemProvider = Mock(ItemProvider) itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')] itemService = new ItemService(itemProvider, eventPublisher) when: def items = itemService.getAllItemsSortedByName(['item-id']) then: items == [new Item('item-id', 'name')] 

We can rewrite the stubbing from the given section:

1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]

So generally, this line says: itemProvider.getItems will be called once with [‘item-‘id'] argument and return given array.

We already know that mocks can behave the same as stubs. All of the rules regarding argument constraints, returning multiple values, and side-effects also apply to Mock.

6. Spying Classes in Spock

Spies provide the ability to wrap an existing object. This means we can listen in on the conversation between the caller and the real object but retain the original object behavior. Basically, Spy delegates method calls to the original object.

In contrast to Mock and Stub, we can't create a Spy on an interface. It wraps an actual object, so additionally, we will need to pass arguments for the constructor. Otherwise, the type's default constructor will be invoked.

6.1. Code Under Test

Let's create a simple implementation for EventPublisher. LoggingEventPublisher will print in the console the id of every added item. Here's the interface method implementation:

@Override public void publish(String addedOfferId) { System.out.println("I've published: " + addedOfferId); }

6.2. Testing with Spy

We create spies similarly to mocks and stubs, by using the Spy(Class) method. LoggingEventPublisher does not have any other class dependencies, so we don't have to pass constructor args:

eventPublisher = Spy(LoggingEventPublisher)

Now, let's test our spy. We need a new instance of ItemService with our spied object:

given: eventPublisher = Spy(LoggingEventPublisher) itemService = new ItemService(itemProvider, eventPublisher) when: itemService.saveItems(['item-id']) then: 1 * eventPublisher.publish('item-id')

We verified that the eventPublisher.publish method was called only once. Additionally, the method call was passed to the real object, so we'll see the output of println in the console:

I've published: item-id

Note that when we use stub on a method of Spy, then it won't call the real object method. Generally, we should avoid using spies. If we have to do it, maybe we should rearrange the code under specification?

7. Good Unit Tests

Let's end with a quick summary of how the use of mocked objects improves our tests:

  • we create deterministic test suites
  • we won't have any side effects
  • our unit tests will be very fast
  • we can focus on the logic contained in a single Java class
  • our tests are independent of the environment

8. Conclusion

Dalam artikel ini, kami menjelaskan secara menyeluruh mata-mata, ejekan, dan rintisan di Groovy . Pengetahuan tentang hal ini akan membuat pengujian kami lebih cepat, lebih andal, dan lebih mudah dibaca.

Penerapan semua contoh kami dapat ditemukan di proyek Github.