Perbezaan Antara Stub, Mock, dan Spy dalam Kerangka Spock

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan membincangkan perbezaan antara Mock , Stub , dan Spy dalam kerangka Spock . Kami akan menerangkan apa yang ditawarkan oleh kerangka kerja berkenaan dengan pengujian berasaskan interaksi.

Spock adalah kerangka pengujian untuk Java dan Groovy yang membantu mengotomatisasi proses pengujian manual aplikasi perisian. Ini memperkenalkan ejekan, rintisan, dan mata-mata sendiri, dan dilengkapi dengan kemampuan terbina dalam untuk ujian yang biasanya memerlukan perpustakaan tambahan.

Pertama, kita akan menerangkan kapan kita harus menggunakan rintisan. Kemudian, kita akan mengejek. Pada akhirnya, kami akan menerangkan Perisik yang baru diperkenalkan .

2. Pergantungan Maven

Sebelum memulakan, mari tambahkan kebergantungan Maven kami:

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

Perhatikan bahawa kami memerlukan versi 1.3-RC1-groovy-2.5 Spock. Spy akan diperkenalkan dalam versi stabil Spock Framework yang seterusnya. Sekarang Spy tersedia dalam calon pelepasan pertama untuk versi 1.3.

Untuk ringkasan struktur asas ujian Spock, lihat artikel pengenalan kami mengenai pengujian dengan Groovy dan Spock.

3. Ujian Berasaskan Interaksi

Pengujian berasaskan interaksi adalah teknik yang membantu kita menguji tingkah laku objek - khususnya, bagaimana mereka berinteraksi antara satu sama lain. Untuk ini, kita dapat menggunakan implementasi palsu yang disebut ejekan dan rintisan.

Sudah tentu, kita dapat dengan mudah menulis pelaksanaan ejekan dan rintisan kita sendiri. Masalahnya muncul apabila jumlah kod pengeluaran kami bertambah. Menulis dan mengekalkan kod ini dengan tangan menjadi sukar. Inilah sebabnya mengapa kami menggunakan rangka kerja mengejek, yang memberikan cara ringkas untuk menerangkan secara ringkas interaksi yang diharapkan. Spock mempunyai sokongan dalaman untuk mengejek, menikam, dan mengintip.

Seperti kebanyakan perpustakaan Java, Spock menggunakan proksi dinamik JDK untuk mengejek antara muka dan Byte Buddy atau proksi cglib untuk kelas mengejek. Ini mewujudkan pelaksanaan tiruan pada waktu runtime.

Java sudah mempunyai banyak perpustakaan yang berbeza dan matang untuk mengejek kelas dan antara muka. Walaupun masing-masing dapat digunakan dalam Spock , masih ada satu alasan utama mengapa kita harus menggunakan ejekan, rintisan, dan mata-mata Spock. Dengan memperkenalkan semua ini ke Spock, kami dapat memanfaatkan semua kemampuan Groovy untuk menjadikan ujian kami lebih mudah dibaca, lebih mudah ditulis, dan pasti lebih menyenangkan!

4. Panggilan Kaedah Stubbing

Kadang-kadang, dalam ujian unit, kita perlu memberikan tingkah laku palsu dalam kelas . Ini mungkin pelanggan untuk perkhidmatan luaran, atau kelas yang menyediakan akses ke pangkalan data. Teknik ini dikenali sebagai stubbing.

Rintisan adalah penggantian terkawal dari kebergantungan kelas yang ada dalam kod kami yang diuji. Ini berguna untuk membuat panggilan kaedah yang bertindak balas dengan cara tertentu. Apabila kita menggunakan rintisan, kita tidak peduli berapa kali kaedah akan digunakan. Sebaliknya, kami hanya ingin mengatakan: kembalikan nilai ini apabila dipanggil dengan data ini.

Mari beralih ke kod contoh dengan logik perniagaan.

4.1. Kod 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 mengatasi kaedah sama (Objek lain) untuk membuat penegasan kita berfungsi. Spock akan menggunakan sama dengan penegasan ketika kita menggunakan tanda sama ganda (==):

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

Sekarang, mari buat antarmuka ItemProvider dengan satu kaedah:

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

Kami juga memerlukan kelas yang akan diuji. Kami akan menambahkan ItemProvider sebagai ketergantungan dalam 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 mahu kod kami bergantung pada pengabstrakan, bukan pelaksanaan tertentu. Itulah sebabnya kami menggunakan antara muka. Ini boleh mempunyai banyak pelaksanaan yang berbeza. Sebagai contoh, kita dapat membaca item dari fail, membuat klien HTTP ke perkhidmatan luaran, atau membaca data dari pangkalan data.

Dalam kod ini, kita perlu menghentikan kebergantungan luaran, kerana kita hanya ingin menguji logik kita yang terdapat dalam kaedah getAllItemsSortedByName .

4.2. Menggunakan Objek Stubbed dalam Kod yang Diuji

Mari kita mulakan objek ItemService dalam kaedah setup () menggunakan Stub untuk ketergantungan ItemProvider :

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

Sekarang, mari buat itemProvider mengembalikan senarai item pada setiap permintaan dengan argumen tertentu :

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

Kami menggunakan >> operand untuk membendung kaedah. The getItems kaedah akan sentiasa kembali senarai dua perkara apabila dipanggil dengan [ 'tawaran-id', 'tawaran-id-2'] senarai. [] adalah jalan pintas Groovy untuk membuat senarai.

Inilah keseluruhan kaedah ujian:

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 kemampuan stubbing yang dapat kita gunakan, seperti: menggunakan batasan pencocokan argumen, menggunakan urutan nilai dalam stub, menentukan tingkah laku yang berbeza dalam keadaan tertentu, dan tindak balas kaedah rantai.

5. Kaedah Mengejek Kelas

Sekarang, mari kita bincangkan kelas atau antara muka mengejek di Spock.

Kadang-kadang, kami ingin mengetahui apakah beberapa kaedah objek bergantung dipanggil dengan argumen yang ditentukan . Kami ingin memberi tumpuan kepada tingkah laku objek dan meneroka bagaimana mereka berinteraksi dengan melihat kaedah panggilan.Mengejek adalah penerangan mengenai interaksi wajib antara objek dalam kelas ujian.

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

5.1. Kod dengan Interaksi

Sebagai contoh mudah, kita akan menyimpan item dalam pangkalan data. Selepas berjaya, kami ingin menerbitkan acara di broker mesej mengenai item baru dalam sistem kami.

Broker pesanan contoh adalah RabbitMQ atau Kafka , jadi secara amnya, kami hanya akan menerangkan kontrak kami:

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

Kaedah ujian kami akan menyimpan item yang tidak kosong dalam pangkalan data dan kemudian menerbitkan acara tersebut. Menyimpan item dalam pangkalan data tidak relevan dalam contoh kami, jadi kami hanya akan memberikan komen:

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

5.2. Mengesahkan Interaksi dengan Objek Mengejek

Sekarang, mari kita uji interaksi dalam kod kita.

Pertama, kita perlu mengejek EventPublisher dalam kaedah setup () kita . Jadi pada dasarnya, kami membuat medan contoh baru dan mengejeknya dengan menggunakan fungsi Mock (Class) :

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 boleh menulis kaedah ujian kita. Kami akan melewati 3 String: ", 'a', 'b' dan kami menjangkakan bahawa 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 perhatikan lebih mendalam pernyataan kita di bahagian akhir kemudian :

1 * eventPublisher.publish('a')

Kami menjangkakan bahawa itemService akan memanggil eventPublisher.publish (String) dengan 'a' sebagai argumen.

Dalam membantutkan, kami telah membincangkan mengenai kekangan hujah. Peraturan yang sama berlaku untuk ejekan. Kita dapat mengesahkan bahawa eventPublisher.publish (String) dipanggil dua kali dengan sebarang argumen yang tidak kosong dan tidak kosong:

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

5.3. Menggabungkan Mengejek dan Menikam

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 menerangkan secara menyeluruh mata-mata, ejekan, dan rintisan di Groovy . Pengetahuan mengenai perkara ini akan menjadikan ujian kita lebih cepat, lebih dipercayai, dan lebih mudah dibaca.

Pelaksanaan semua contoh kami boleh didapati dalam projek Github.