Pengenalan Menguji dengan Spock dan Groovy

1. Pengenalan

Dalam artikel ini, kita akan melihat Spock, kerangka ujian Groovy. Terutamanya, Spock bertujuan untuk menjadi alternatif yang lebih kuat daripada timbunan JUnit tradisional, dengan memanfaatkan ciri Groovy.

Groovy adalah bahasa berasaskan JVM yang berintegrasi dengan Java dengan lancar. Di atas kebolehoperasian, ia menawarkan konsep bahasa tambahan seperti dinamik, mempunyai jenis pilihan dan meta-pengaturcaraan.

Dengan memanfaatkan Groovy, Spock memperkenalkan cara baru dan ekspresif untuk menguji aplikasi Java kami, yang tidak mungkin dilakukan dalam kod Java biasa. Kami akan meneroka beberapa konsep peringkat tinggi Spock semasa artikel ini, dengan beberapa contoh langkah demi langkah praktikal.

2. Ketergantungan Maven

Sebelum memulakan, mari tambahkan kebergantungan Maven kami:

 org.spockframework spock-core 1.0-groovy-2.4 test   org.codehaus.groovy groovy-all 2.4.7 test 

Kami telah menambah kedua Spock dan Groovy seperti mana-mana perpustakaan standard. Namun, kerana Groovy adalah bahasa JVM baru, kita perlu memasukkan plugin gmavenplus agar dapat menyusun dan menjalankannya:

 org.codehaus.gmavenplus gmavenplus-plugin 1.5    compile testCompile    

Sekarang kami bersedia untuk menulis ujian Spock pertama kami, yang akan ditulis dalam kod Groovy. Perhatikan bahawa kami menggunakan Groovy dan Spock hanya untuk tujuan ujian dan inilah sebabnya kebergantungan tersebut dilindungi ujian.

3. Struktur Ujian Spock

3.1. Spesifikasi dan Ciri

Semasa kami menulis ujian kami di Groovy, kami perlu menambahkannya ke direktori src / test / groovy , bukan src / test / java. Mari buat ujian pertama kami di direktori ini, namakan Spesifikasi.groovy:

class FirstSpecification extends Specification { }

Perhatikan bahawa kami memperluaskan antara muka Spesifikasi . Setiap kelas Spock mesti memperluas ini untuk menjadikan kerangka kerja tersedia untuknya. Ini membolehkan kami melaksanakan ciri pertama kami :

def "one plus one should equal two"() { expect: 1 + 1 == 2 }

Sebelum menjelaskan kodnya, perlu diingat bahawa di Spock, apa yang kita sebut sebagai ciri agak sinonim dengan apa yang kita lihat sebagai ujian di JUnit. Oleh itu, setiap kali kita merujuk kepada satu ciri, kita sebenarnya merujuk kepada ujian.

Sekarang, mari kita analisis ciri kami . Dengan berbuat demikian, kita seharusnya dapat melihat beberapa perbezaan antara Java dan Java.

Perbezaan pertama ialah nama kaedah ciri ditulis sebagai rentetan biasa. Di JUnit, kita akan mempunyai nama kaedah yang menggunakan camelcase atau garis bawah untuk memisahkan kata-kata, yang tidak semestinya ekspresif atau dapat dibaca oleh manusia.

Yang berikutnya adalah bahawa kod ujian kami tinggal di blok jangkaan . Kami akan merangkumi blok dengan lebih terperinci tidak lama lagi, tetapi pada asasnya ia adalah kaedah yang logik untuk membelah pelbagai langkah ujian kami.

Akhirnya, kita menyedari bahawa tidak ada penegasan. Itu kerana penegasan itu tersirat, berlaku ketika pernyataan kita sama dengan benar dan gagal ketika sama dengan palsu . Sekali lagi, kami akan merangkumi pernyataan dengan lebih terperinci tidak lama lagi.

3.2. Blok

Kadang-kadang semasa menulis ujian JUnit, kita mungkin menyedari tidak ada cara ekspresif untuk memecahnya menjadi beberapa bahagian. Sebagai contoh, jika kita mengikuti perkembangan yang didorong oleh tingkah laku, kita mungkin akhirnya menunjukkan bahagian yang diberikan ketika itu menggunakan komen:

@Test public void givenTwoAndTwo_whenAdding_thenResultIsFour() { // Given int first = 2; int second = 4; // When int result = 2 + 2; // Then assertTrue(result == 4) }

Spock mengatasi masalah ini dengan blok. Blok adalah cara asli Spock untuk memecahkan fasa ujian kami menggunakan label. Mereka memberi kita label untuk diberikan ketika dan seterusnya :

  1. Penyediaan (Diasingkan oleh Diberikan) - Di sini kita melakukan penyediaan yang diperlukan sebelum ujian dijalankan. Ini adalah blok tersirat, dengan kod sama sekali tidak menjadi sebahagian daripadanya
  2. Bila - Di sinilah kita memberikan rangsangan kepada apa yang sedang diuji. Dengan kata lain, di mana kita menggunakan kaedah kita yang sedang diuji
  3. Kemudian - Di sinilah penegasan tersebut. Dalam Spock, ini dinilai sebagai pernyataan boolean biasa, yang akan dibahas kemudian
  4. Harapkan - Ini adalah cara melaksanakan rangsangan dan penegasan kita dalam blok yang sama. Bergantung pada apa yang kami rasa lebih ekspresif, kami mungkin atau tidak memilih untuk menggunakan blok ini
  5. Pembersihan - Di sini kami meruntuhkan sebarang sumber ketergantungan ujian yang sebaliknya akan ditinggalkan. Sebagai contoh, kami mungkin ingin membuang fail dari sistem fail atau membuang data ujian yang ditulis ke pangkalan data

Mari cuba laksanakan ujian kami sekali lagi, kali ini menggunakan blok sepenuhnya:

def "two plus two should equal four"() { given: int left = 2 int right = 2 when: int result = left + right then: result == 4 }

Seperti yang kita lihat, blok membantu ujian kita menjadi lebih mudah dibaca.

3.3. Memanfaatkan Ciri Groovy untuk Tegasan

Dalam sekatan kemudian dan jangkaan , penegasan tersirat .

Selalunya, setiap pernyataan dinilai dan kemudian gagal sekiranya tidak benar . Apabila menggabungkan ini dengan pelbagai ciri Groovy, ia berfungsi dengan baik untuk menghilangkan keperluan untuk perpustakaan penegasan. Mari cuba pernyataan senarai untuk menunjukkan ini:

def "Should be able to remove from list"() { given: def list = [1, 2, 3, 4] when: list.remove(0) then: list == [2, 3, 4] }

Walaupun kita hanya menyentuh sebentar mengenai Groovy dalam artikel ini, perlu dijelaskan apa yang berlaku di sini.

Pertama, Groovy memberi kita kaedah yang lebih mudah untuk membuat senarai. Kita hanya dapat menyatakan elemen kita dengan tanda kurung, dan secara dalaman senarai akan dibuat.

Kedua, kerana Groovy bersifat dinamik, kita dapat menggunakan def yang bermaksud kita tidak menyatakan jenis pemboleh ubah kita.

Akhirnya, dalam konteks mempermudah ujian kami, ciri yang paling berguna yang ditunjukkan adalah kelebihan operator. Ini bermaksud bahawa secara dalaman, daripada membuat perbandingan rujukan seperti di Java, kaedah sama () akan dipanggil untuk membandingkan kedua daftar tersebut.

Ia juga patut menunjukkan apa yang berlaku apabila ujian kita gagal. Mari buat pecah dan kemudian lihat apa output ke konsol:

Condition not satisfied: list == [1, 3, 4] | | | false [2, 3, 4]  at FirstSpecification.Should be able to remove from list(FirstSpecification.groovy:30)

Walaupun semua yang terjadi adalah memanggil sama () dalam dua senarai, Spock cukup pintar untuk melakukan perincian penegasan yang gagal, memberikan kami maklumat berguna untuk melakukan debug.

3.4. Menegaskan Pengecualian

Spock also provides us with an expressive way of checking for exceptions. In JUnit, some our options might be using a try-catch block, declare expected at the top of our test, or making use of a third party library. Spock's native assertions come with a way of dealing with exceptions out of the box:

def "Should get an index out of bounds when removing a non-existent item"() { given: def list = [1, 2, 3, 4] when: list.remove(20) then: thrown(IndexOutOfBoundsException) list.size() == 4 }

Here, we've not had to introduce an additional library. Another advantage is that the thrown() method will assert the type of the exception, but not halt execution of the test.

4. Data Driven Testing

4.1. What Is a Data Driven Testing?

Essentially, data driven testing is when we test the same behavior multiple times with different parameters and assertions. A classic example of this would be testing a mathematical operation such as squaring a number. Depending on the various permutations of operands, the result will be different. In Java, the term we may be more familiar with is parameterized testing.

4.2. Implementing a Parameterized Test in Java

For some context, it's worth implementing a parameterized test using JUnit:

@RunWith(Parameterized.class) public class FibonacciTest { @Parameters public static Collection data() { return Arrays.asList(new Object[][] { { 1, 1 }, { 2, 4 }, { 3, 9 } }); } private int input; private int expected; public FibonacciTest (int input, int expected) { this.input = input; this.expected = expected; } @Test public void test() { assertEquals(fExpected, Math.pow(3, 2)); } }

As we can see there's quite a lot of verbosity, and the code isn't very readable. We've had to create a two-dimensional object array that lives outside of the test, and even a wrapper object for injecting the various test values.

4.3. Using Datatables in Spock

One easy win for Spock when compared to JUnit is how it cleanly it implements parameterized tests. Again, in Spock, this is known as Data Driven Testing. Now, let's implement the same test again, only this time we'll use Spock with Data Tables, which provides a far more convenient way of performing a parameterized test:

def "numbers to the power of two"(int a, int b, int c)  4 3 

As we can see, we just have a straightforward and expressive Data table containing all our parameters.

Also, it belongs where it should do, alongside the test, and there is no boilerplate. The test is expressive, with a human-readable name, and pure expect and where block to break up the logical sections.

4.4. When a Datatable Fails

It's also worth seeing what happens when our test fails:

Condition not satisfied: Math.pow(a, b) == c | | | | | 4.0 2 2 | 1 false Expected :1 Actual :4.0

Again, Spock gives us a very informative error message. We can see exactly what row of our Datatable caused a failure and why.

5. Mocking

5.1. What Is Mocking?

Mocking is a way of changing the behavior of a class which our service under test collaborates with. It's a helpful way of being able to test business logic in isolation of its dependencies.

A classic example of this would be replacing a class which makes a network call with something which simply pretends to. For a more in-depth explanation, it's worth reading this article.

5.2. Mocking Using Spock

Spock has it's own mocking framework, making use of interesting concepts brought to the JVM by Groovy. First, let's instantiate a Mock:

PaymentGateway paymentGateway = Mock()

In this case, the type of our mock is inferred by the variable type. As Groovy is a dynamic language, we can also provide a type argument, allow us to not have to assign our mock to any particular type:

def paymentGateway = Mock(PaymentGateway)

Now, whenever we call a method on our PaymentGateway mock, a default response will be given, without a real instance being invoked:

when: def result = paymentGateway.makePayment(12.99) then: result == false

The term for this is lenient mocking. This means that mock methods which have not been defined will return sensible defaults, as opposed to throwing an exception. This is by design in Spock, in order to make mocks and thus tests less brittle.

5.3. Stubbing Method Calls on Mocks

We can also configure methods called on our mock to respond in a certain way to different arguments. Let's try getting our PaymentGateway mock to return true when we make a payment of 20:

given: paymentGateway.makePayment(20) >> true when: def result = paymentGateway.makePayment(20) then: result == true

What's interesting here, is how Spock makes use of Groovy's operator overloading in order to stub method calls. With Java, we have to call real methods, which arguably means that the resulting code is more verbose and potentially less expressive.

Now, let's try a few more types of stubbing.

If we stopped caring about our method argument and always wanted to return true, we could just use an underscore:

paymentGateway.makePayment(_) >> true

If we wanted to alternate between different responses, we could provide a list, for which each element will be returned in sequence:

paymentGateway.makePayment(_) >>> [true, true, false, true]

There are more possibilities, and these may be covered in a more advanced future article on mocking.

5.4. Verification

Another thing we might want to do with mocks is assert that various methods were called on them with expected parameters. In other words, we ought to verify interactions with our mocks.

A typical use case for verification would be if a method on our mock had a void return type. In this case, by there being no result for us to operate on, there's no inferred behavior for us to test via the method under test. Generally, if something was returned, then the method under test could operate on it, and it's the result of that operation would be what we assert.

Let's try verifying that a method with a void return type is called:

def "Should verify notify was called"() { given: def notifier = Mock(Notifier) when: notifier.notify('foo') then: 1 * notifier.notify('foo') } 

Spock is leveraging Groovy operator overloading again. By multiplying our mocks method call by one, we are saying how many times we expect it to have been called.

If our method had not been called at all or alternatively had not been called as many times as we specified, then our test would have failed to give us an informative Spock error message. Let's prove this by expecting it to have been called twice:

2 * notifier.notify('foo')

Following this, let's see what the error message looks like. We'll that as usual; it's quite informative:

Too few invocations for: 2 * notifier.notify('foo') (1 invocation)

Just like stubbing, we can also perform looser verification matching. If we didn't care what our method parameter was, we could use an underscore:

2 * notifier.notify(_)

Or if we wanted to make sure it wasn't called with a particular argument, we could use the not operator:

2 * notifier.notify(!'foo')

Sekali lagi, ada lebih banyak kemungkinan, yang mungkin akan dibahas dalam artikel yang lebih maju di masa hadapan.

6. Kesimpulannya

Dalam artikel ini, kami telah memberikan potongan ringkas melalui ujian dengan Spock.

Kami telah menunjukkan bagaimana, dengan memanfaatkan Groovy, kami dapat menjadikan ujian kami lebih ekspresif daripada timbunan JUnit biasa. Kami telah menjelaskan struktur spesifikasi dan ciri .

Dan kami telah menunjukkan betapa mudahnya melakukan pengujian berdasarkan data, dan juga bagaimana ejekan dan penegasan mudah dilakukan melalui fungsi Spock asli.

Pelaksanaan contoh-contoh ini boleh didapati di GitHub. Ini adalah projek berasaskan Maven, jadi semestinya mudah dijalankan sebagaimana adanya.