Praktik terbaik JUnit

JUnit adalah toolkit yang khas: jika digunakan dengan hati-hati dan dengan mengenali keistimewaannya, JUnit akan membantu mengembangkan pengujian yang baik dan kuat. Jika digunakan secara membabi buta, alat ini mungkin akan menghasilkan tumpukan spageti alih-alih rangkaian percobaan. Artikel ini menyajikan beberapa pedoman yang dapat membantu Anda menghindari mimpi buruk pasta. Pedoman tersebut terkadang bertentangan satu sama lain - ini disengaja. Menurut pengalaman saya, jarang ada aturan keras dan cepat dalam pengembangan, dan pedoman yang diklaim menyesatkan.

Kami juga akan memeriksa dengan cermat dua tambahan yang berguna untuk perangkat pengembang:

  • Mekanisme untuk secara otomatis membuat rangkaian pengujian dari file kelas di bagian sistem file
  • Yang baru TestCaseyang lebih mendukung pengujian di banyak utas

Saat dihadapkan dengan pengujian unit, banyak tim akhirnya menghasilkan semacam kerangka pengujian. JUnit, tersedia sebagai open source, menghilangkan tugas berat ini dengan menyediakan kerangka kerja siap pakai untuk pengujian unit. JUnit, paling baik digunakan sebagai bagian integral dari rezim pengujian pengembangan, menyediakan mekanisme yang dapat digunakan developer untuk menulis dan menjalankan pengujian secara konsisten. Jadi, apa praktik terbaik JUnit?

Jangan gunakan konstruktor kasus uji untuk menyiapkan kasus uji

Menyiapkan kasus uji di konstruktor bukanlah ide yang baik. Mempertimbangkan:

kelas publik SomeTest memperluas TestCase publik SomeTest (String testName) {super (testName); // Lakukan tes set-up}}

Bayangkan saat melakukan penyiapan, kode penyiapan melempar IllegalStateException. Sebagai tanggapan, JUnit akan melempar AssertionFailedError, yang menunjukkan bahwa kasus uji tidak dapat dibuat instance-nya. Berikut adalah contoh jejak tumpukan yang dihasilkan:

junit.framework.AssertionFailedError: Tidak dapat membuat contoh kasus uji: test1 di junit.framework.Assert.fail (Assert.java:143) di junit.framework.TestSuite.runTest (TestSuite.java:178) di junit.framework.TestCase.runBare (TestCase.java:129) di junit.framework.TestResult.protect (TestResult.java:100) di junit.framework.TestResult.runProtected (TestResult.java:117) di junit.framework.TestResult.run (TestResult.java: 103) di junit.framework.TestCase.run (TestCase.java:120) di junit.framework.TestSuite.run (TestSuite.java, Kode Terkompilasi) di junit.ui.TestRunner2.run (TestRunner.java:429) 

Pelacakan tumpukan ini terbukti agak tidak informatif; itu hanya menunjukkan bahwa kasus uji tidak dapat dipakai. Itu tidak merinci lokasi atau tempat asal kesalahan asli. Kurangnya informasi ini membuat sulit untuk menyimpulkan penyebab yang mendasari pengecualian tersebut.

Alih-alih menyiapkan data di konstruktor, lakukan penyiapan pengujian dengan mengganti setUp(). Setiap pengecualian yang dilemparkan ke dalam setUp()dilaporkan dengan benar. Bandingkan pelacakan tumpukan ini dengan contoh sebelumnya:

java.lang.IllegalStateException: Ups di bp.DTC.setUp (DTC.java:34) di junit.framework.TestCase.runBare (TestCase.java:127) di junit.framework.TestResult.protect (TestResult.java:100) di junit.framework.TestResult.runProtected (TestResult.java:117) di junit.framework.TestResult.run (TestResult.java:103) ... 

Pelacakan tumpukan ini jauh lebih informatif; itu menunjukkan pengecualian mana yang dilempar ( IllegalStateException) dan dari mana. Itu membuatnya jauh lebih mudah untuk menjelaskan kegagalan penyiapan pengujian.

Jangan berasumsi urutan pengujian dalam kasus pengujian yang dijalankan

Anda tidak boleh berasumsi bahwa tes akan dipanggil dalam urutan tertentu. Pertimbangkan segmen kode berikut:

kelas publik SomeTestCase memperluas TestCase {publik SomeTestCase (String testName) {super (testName); } public void testDoThisFirst () {...} public void testDoThisSecond () {}}

Dalam contoh ini, tidak ada kepastian bahwa JUnit akan menjalankan pengujian ini dalam urutan tertentu saat menggunakan refleksi. Oleh karena itu, menjalankan pengujian pada platform yang berbeda dan VM Java dapat memberikan hasil yang berbeda, kecuali pengujian Anda dirancang untuk dijalankan dalam urutan apa pun. Menghindari penggandengan sementara akan membuat kasus uji lebih kuat, karena perubahan urutan tidak akan memengaruhi pengujian lainnya. Jika pengujian digabungkan, kesalahan yang dihasilkan dari pembaruan kecil mungkin sulit ditemukan.

Dalam situasi di mana pengujian pengurutan masuk akal - ketika pengujian lebih efisien untuk beroperasi pada beberapa data bersama yang menetapkan status baru saat setiap pengujian berjalan - gunakan suite()metode statis seperti ini untuk memastikan pengurutan:

public static Test suite () {suite.addTest (new SomeTestCase ("testDoThisFirst";)); suite.addTest (new SomeTestCase ("testDoThisSecond";)); suite kembali; }

Tidak ada jaminan dalam dokumentasi JUnit API mengenai urutan pengujian Anda akan dipanggil, karena JUnit menggunakan Vectoruntuk menyimpan pengujian. Namun, Anda dapat mengharapkan pengujian di atas dijalankan agar pengujian tersebut ditambahkan ke rangkaian pengujian.

Hindari menulis kasus uji dengan efek samping

Kasus uji yang memiliki efek samping menunjukkan dua masalah:

  • Mereka dapat memengaruhi data yang diandalkan oleh kasus uji lainnya
  • Anda tidak dapat mengulangi tes tanpa intervensi manual

Dalam situasi pertama, kasus uji individu dapat beroperasi dengan benar. Namun, jika digabungkan ke dalam TestSuiteyang menjalankan setiap kasus pengujian pada sistem, ini dapat menyebabkan kasus pengujian lain gagal. Mode kegagalan itu bisa sulit untuk didiagnosis, dan kesalahan mungkin terletak jauh dari kegagalan pengujian.

Dalam situasi kedua, kasus uji mungkin telah memperbarui beberapa status sistem sehingga tidak dapat berjalan lagi tanpa intervensi manual, yang mungkin terdiri dari menghapus data uji dari database (misalnya). Pikirkan baik-baik sebelum memperkenalkan intervensi manual. Pertama, intervensi manual perlu didokumentasikan. Kedua, pengujian tidak dapat lagi dijalankan dalam mode tanpa pengawasan, menghilangkan kemampuan Anda untuk menjalankan pengujian dalam semalam atau sebagai bagian dari beberapa pengujian berkala otomatis.

Panggil metode setUp () dan tearDown () superclass saat membuat subclass

Ketika Anda mempertimbangkan:

public class SomeTestCase extends AnotherTestCase {// Koneksi ke database private Database theDatabase; public SomeTestCase (String testName) {super (testName); } public void testFeatureX () {...} public void setUp () {// Hapus database theDatabase.clear (); }}

Bisakah Anda melihat kesalahan yang disengaja? setUp()harus memanggil super.setUp()untuk memastikan bahwa lingkungan yang ditentukan dalam AnotherTestCasemenginisialisasi. Tentu saja, ada pengecualian: jika Anda mendesain kelas dasar untuk bekerja dengan data pengujian yang berubah-ubah, tidak akan ada masalah.

Jangan memuat data dari lokasi hard-code pada sistem file

Pengujian sering kali perlu memuat data dari beberapa lokasi di sistem file. Pertimbangkan hal berikut:

public void setUp () {FileInputStream inp ("C: \\ TestData \\ dataSet1.dat"); ...}

Kode di atas bergantung pada kumpulan data yang ada di C:\TestDatajalur. Asumsi itu salah dalam dua situasi:

  • Penguji tidak memiliki ruang untuk menyimpan data pengujian C:dan menyimpannya di disk lain
  • Pengujian dijalankan di platform lain, seperti Unix

Salah satu solusinya mungkin:

public void setUp () {FileInputStream inp ("dataSet1.dat"); ...}

Namun, solusi tersebut bergantung pada pengujian yang dijalankan dari direktori yang sama dengan data pengujian. Jika beberapa kasus pengujian yang berbeda mengasumsikan hal ini, maka sulit untuk mengintegrasikannya ke dalam satu rangkaian pengujian tanpa terus mengubah direktori saat ini.

Untuk mengatasi masalah ini, akses set data menggunakan salah satu Class.getResource()atau Class.getResourceAsStream(). Namun, menggunakannya berarti resource memuat dari lokasi yang relatif terhadap asal kelas.

Data pengujian harus, jika memungkinkan, disimpan dengan kode sumber dalam sistem manajemen konfigurasi (CM). Namun, jika Anda menggunakan mekanisme resource yang disebutkan di atas, Anda harus menulis skrip yang memindahkan semua data pengujian dari sistem CM ke jalur kelas dari sistem yang diuji. Pendekatan yang tidak terlalu kaku adalah dengan menyimpan data pengujian di pohon sumber bersama dengan file sumber. Dengan pendekatan ini, Anda memerlukan mekanisme lokasi-independen untuk menemukan data pengujian dalam pohon sumber. Salah satu mekanisme tersebut adalah kelas. Jika kelas dapat dipetakan ke direktori sumber tertentu, Anda dapat menulis kode seperti ini:

InputStream inp = SourceResourceLoader.getResourceAsStream (this.getClass (), "dataSet1.dat"); 

Sekarang Anda hanya perlu menentukan cara memetakan dari kelas ke direktori yang berisi file sumber yang relevan. Anda dapat mengidentifikasi akar dari pohon sumber (dengan asumsi ia memiliki satu akar) dengan properti sistem. Nama paket kelas kemudian dapat mengidentifikasi direktori tempat file sumber berada. Sumber daya dimuat dari direktori itu. Untuk Unix dan NT, pemetaannya sangat mudah: ganti setiap instance '.' dengan File.separatorChar.

Pertahankan pengujian di lokasi yang sama dengan kode sumber

Jika sumber pengujian disimpan di lokasi yang sama dengan kelas yang diuji, pengujian dan kelas akan dikompilasi selama pembuatan. Ini memaksa Anda untuk menjaga tes dan kelas tetap sinkron selama pengembangan. Memang, pengujian unit yang tidak dianggap sebagai bagian dari build normal dengan cepat menjadi usang dan tidak berguna.

Tes nama dengan benar

Name the test case TestClassUnderTest. For example, the test case for the class MessageLog should be TestMessageLog. That makes it simple to work out what class a test case tests. Test methods' names within the test case should describe what they test:

  • testLoggingEmptyMessage()
  • testLoggingNullMessage()
  • testLoggingWarningMessage()
  • testLoggingErrorMessage()

Proper naming helps code readers understand each test's purpose.

Ensure that tests are time-independent

Where possible, avoid using data that may expire; such data should be either manually or programmatically refreshed. It is often simpler to instrument the class under test, with a mechanism for changing its notion of today. The test can then operate in a time-independent manner without having to refresh the data.

Consider locale when writing tests

Consider a test that uses dates. One approach to creating dates would be:

Date date = DateFormat.getInstance ().parse ("dd/mm/yyyy"); 

Unfortunately, that code doesn't work on a machine with a different locale. Therefore, it would be far better to write:

Calendar cal = Calendar.getInstance (); Cal.set (yyyy, mm-1, dd); Date date = Calendar.getTime (); 

The second approach is far more resilient to locale changes.

Utilize JUnit's assert/fail methods and exception handling for clean test code

Many JUnit novices make the mistake of generating elaborate try and catch blocks to catch unexpected exceptions and flag a test failure. Here is a trivial example of this:

public void exampleTest () { try { // do some test } catch (SomeApplicationException e) { fail ("Caught SomeApplicationException exception"); } } 

JUnit automatically catches exceptions. It considers uncaught exceptions to be errors, which means the above example has redundant code in it.

Here's a far simpler way to achieve the same result:

public void exampleTest () throws SomeApplicationException { // do some test } 

In this example, the redundant code has been removed, making the test easier to read and maintain (since there is less code).

Use the wide variety of assert methods to express your intention in a simpler fashion. Instead of writing:

assert (creds == 3); 

Write:

assertEquals ("The number of credentials should be 3", 3, creds); 

The above example is much more useful to a code reader. And if the assertion fails, it provides the tester with more information. JUnit also supports floating point comparisons:

assertEquals ("some message", result, expected, delta); 

When you compare floating point numbers, this useful function saves you from repeatedly writing code to compute the difference between the result and the expected value.

Use assertSame() to test for two references that point to the same object. Use assertEquals() to test for two objects that are equal.

Document tests in javadoc

Test plans documented in a word processor tend to be error-prone and tedious to create. Also, word-processor-based documentation must be kept synchronized with the unit tests, adding another layer of complexity to the process. If possible, a better solution would be to include the test plans in the tests' javadoc, ensuring that all test plan data reside in one place.

Avoid visual inspection

Testing servlets, user interfaces, and other systems that produce complex output is often left to visual inspection. Visual inspection -- a human inspecting output data for errors -- requires patience, the ability to process large quantities of information, and great attention to detail: attributes not often found in the average human being. Below are some basic techniques that will help reduce the visual inspection component of your test cycle.

Swing

When testing a Swing-based UI, you can write tests to ensure that:

  • All the components reside in the correct panels
  • You've configured the layout managers correctly
  • Text widgets have the correct fonts

A more thorough treatment of this can be found in the worked example of testing a GUI, referenced in the Resources section.

XML

Saat menguji kelas yang memproses XML, sebaiknya tulis rutinitas yang membandingkan dua XML DOM untuk persamaan. Anda kemudian dapat menentukan DOM yang benar secara terprogram sebelumnya dan membandingkannya dengan keluaran sebenarnya dari metode pemrosesan Anda.

Pelayan

Dengan servlet, beberapa pendekatan dapat bekerja. Anda dapat menulis kerangka kerja servlet dan mengkonfigurasinya sebelumnya selama pengujian. Kerangka kerja harus berisi turunan kelas yang ditemukan di lingkungan servlet normal. Derivasi ini akan memungkinkan Anda untuk melakukan prakonfigurasi responsnya ke panggilan metode dari servlet.

Sebagai contoh: