Keuntungan / Kerugian memiliki semua variabel dinyatakan dalam Tes JUnit

9

Saya telah menulis beberapa tes unit untuk beberapa kode baru di kantor, dan mengirimkannya untuk tinjauan kode. Salah satu rekan kerja saya membuat komentar tentang mengapa saya meletakkan variabel yang digunakan dalam sejumlah tes di luar ruang lingkup untuk tes.

Kode yang saya posting pada dasarnya

import org.junit.Test;

public class FooUnitTests {

    @Test
    public void testConstructorWithValidName() {
        new Foo(VALID_NAME);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithNullName() {
        new Foo(null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithZeroLengthName() {
        new Foo("");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
        final String name = " " + VALID_NAME + " ";
        final Foo foo = new Foo(name);

        assertThat(foo.getName(), is(equalTo(VALID_NAME)));
    }

    private static final String VALID_NAME = "name";
}

Usulan perubahannya pada dasarnya

import org.junit.Test;

public class FooUnitTests {

    @Test
    public void testConstructorWithValidName() {
        final String name = "name";
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithNullName() {
        final String name = null;
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithZeroLengthName() {
        final String name = "";
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
        final String name = " name ";
        final Foo foo = new Foo(name);

        final String actual = foo.getName();
        final String expected = "name";

        assertThat(actual, is(equalTo(expected)));
    }
}

Di mana segala sesuatu yang diperlukan dalam ruang lingkup tes, didefinisikan dalam ruang lingkup tes.

Beberapa keuntungan yang ia perdebatkan adalah

  1. Setiap tes mandiri.
  2. Setiap tes dapat dieksekusi dalam isolasi, atau dalam agregasi, dengan hasil yang sama.
  3. Peninjau tidak harus menggulir ke mana pun parameter ini dideklarasikan untuk mencari nilainya.

Beberapa kelemahan dari metodenya yang saya katakan adalah

  1. Meningkatkan duplikasi kode
  2. Dapat menambahkan noise ke pikiran pengulas jika ada beberapa tes serupa dengan nilai yang berbeda ditentukan (mis. Tes dengan doFoo(bar)hasil dalam satu nilai, sementara panggilan yang sama memiliki hasil yang berbeda karena bardidefinisikan berbeda dalam metode itu).

Selain dari konvensi, apakah ada kelebihan / kekurangan menggunakan metode yang lain?

Zymus
sumber
Satu-satunya masalah yang saya lihat dengan kode Anda adalah Anda mengubur pernyataan VALID_NAME di bagian bawah sehingga kami bertanya-tanya dari mana asalnya. Memperbaiki itu akan menangani "pengguliran". Nama lingkup lokal rekan kerja Anda melanggar KERING tanpa alasan. Tanyakan kepadanya mengapa tidak masalah menggunakan pernyataan impor yang berada di luar setiap metode pengujian. Sheesh itu disebut konteks.
candied_orange
@CandiedOrange: apakah tidak ada orang di sini yang membaca jawaban saya? Pendekatan rekan kerja dapat melanggar prinsip KERING, tetapi tidak dalam contoh ini. Tidak ada gunanya memiliki "nama" teks yang sama dalam semua kasus ini.
Doc Brown
Variabel-variabel tambahan tidak menambahkan kejelasan, namun hal itu menunjukkan bahwa Anda dapat menulis ulang tes Anda untuk menggunakan Teori dan DataPoint Junit, yang akan sangat menurunkan tes Anda!
Rosa
1
@CandiedOrange: KERING kurang berkaitan dengan menghindari duplikasi kode daripada tentang menghindari duplikasi pengetahuan (fakta, aturan, dll). Lihat: hermanradtke.com/2013/02/06/misunderstanding-dry.html Prinsip Dry dinyatakan sebagai "Setiap bagian pengetahuan harus memiliki perwakilan tunggal, tidak ambigu, otoritatif dalam suatu sistem." en.wikipedia.org/wiki/Don%27t_repeat_yourself
Marjan Venema
"Setiap bagian pengetahuan harus memiliki representasi tunggal, tidak ambigu, otoritatif dalam suatu sistem." Jadi fakta bahwa "nama" adalah VALID_NAME BUKAN salah satu dari pengetahuan ini? Memang benar KERING lebih dari sekadar menghindari duplikasi kode tetapi Anda akan merasa KERING dengan kode rangkap.
candied_orange

Jawaban:

10

Anda harus terus melakukan apa yang Anda lakukan.

Mengulangi diri Anda sama buruknya dengan gagasan dalam kode uji seperti dalam kode bisnis, dan untuk alasan yang sama. Rekan Anda telah disesatkan oleh gagasan bahwa setiap tes harus mandiri . Itu cukup benar, tetapi "mandiri" tidak berarti harus berisi semua yang dibutuhkan dalam tubuh metodenya. Ini hanya berarti bahwa itu harus memberikan hasil yang sama apakah itu dijalankan secara terpisah atau sebagai bagian dari suite, dan terlepas dari urutan tes di suite. Dengan kata lain, kode tes harus memiliki semantik yang sama terlepas dari kode apa yang telah dijalankan sebelumnya ; itu tidak harus memiliki semua kode yang diperlukan dibundel secara tekstual .

Penggunaan kembali konstanta, kode pengaturan dan sejenisnya meningkatkan kualitas kode uji dan tidak membahayakan kemandiriannya, jadi sebaiknya Anda terus melakukannya.

Kilian Foth
sumber
+1 benar-benar, tetapi harap diingat bahwa KERING lebih sedikit tentang menghindari kode berulang daripada tentang tidak mengulangi pengetahuan. Lihat hermanradtke.com/2013/02/06/misunderstanding-dry.html . Prinsip Kering dinyatakan sebagai "Setiap bagian pengetahuan harus memiliki representasi tunggal, tidak ambigu, otoritatif dalam suatu sistem." en.wikipedia.org/wiki/Don%27t_repeat_yourself
Marjan Venema
3

Tergantung. Secara umum, memiliki konstanta berulang yang dinyatakan hanya di satu tempat adalah hal yang baik.

Namun, dalam contoh Anda, tidak ada gunanya mendefinisikan anggota VALID_NAMEdengan cara yang ditunjukkan. Diasumsikan dalam varian kedua, seorang pengelola mengubah teks namepada tes pertama, tes kedua kemungkinan besar akan tetap valid, dan sebaliknya. Sebagai contoh, mari kita asumsikan Anda juga ingin menguji huruf besar dan kecil, tetapi juga menyimpan kasus uji hanya dengan huruf kecil, dengan sesedikit mungkin kasus uji. Alih-alih menambahkan tes baru, Anda dapat mengubah tes pertama menjadi

public void testConstructorWithValidName() {
    final String name = "Name";
    final Foo foo = new Foo(name);
}

dan masih menyimpan tes yang tersisa.

Bahkan, memiliki banyak tes tergantung pada variabel seperti itu, dan mengubah nilai VALID_NAMEnanti mungkin secara tidak sengaja merusak beberapa tes Anda di waktu kemudian. Dan dalam situasi seperti itu, Anda memang dapat meningkatkan kemandirian tes Anda dengan tidak memperkenalkan konstanta buatan dengan tes yang berbeda tergantung pada itu.

Namun, apa yang ditulis @KilianFoth tentang tidak mengulangi diri Anda sendiri juga benar: ikuti prinsip KERING dalam kode uji seperti halnya Anda melakukannya dalam kode bisnis. Misalnya, perkenalkan konstanta atau variabel anggota ketika penting untuk kode pengujian Anda bahwa nilainya sama di setiap tempat.

Contoh Anda menunjukkan bagaimana tes cenderung mengulang kode inisialisasi, itu adalah tempat khas untuk memulai dengan refactoring. Jadi, Anda dapat mempertimbangkan untuk mengulangi pengulangan

  new Foo(...);

menjadi fungsi yang terpisah

 public Foo ConstructFoo(String name) 
 {
     return new Foo(name);
 }

karena itu akan memungkinkan Anda untuk mengubah tanda tangan Fookonstruktor pada titik waktu lebih mudah (karena ada lebih sedikit tempat di mana new Foosebenarnya disebut).

Doc Brown
sumber
Ini hanya contoh kode yang sangat singkat. Dalam kode produksi, variabel VALID_NAME digunakan ~ 30 kali. Namun itu adalah poin yang sangat makanan dan yang saya tidak miliki.
Zymus
@Zymus: berapa kali VALID_NAME digunakan adalah IMHO tidak relevan - yang penting adalah jika tes Anda penting untuk menggunakan teks "nama" yang sama dalam semua kasus (jadi masuk akal untuk memperkenalkan simbol untuk itu), atau jika memperkenalkan simbol menciptakan ketergantungan buatan yang tidak dibutuhkan. Saya hanya bisa melihat contoh Anda yang merupakan IMHO dari tipe kedua, tetapi untuk kode "asli" Anda, jarak tempuh Anda mungkin berbeda-beda.
Doc Brown
1
+1 untuk kasing yang Anda buat. Dan kode uji harus kode tingkat produksi. Yang mengatakan, KERING bukan tentang tidak mengulangi diri Anda sama sekali. Ini tentang tidak mengulangi pengetahuan. Prinsip KERING dinyatakan sebagai "Setiap bagian pengetahuan harus memiliki representasi tunggal, tidak ambigu, otoritatif dalam suatu sistem." ( en.wikipedia.org/wiki/Don%27t_repeat_yourself ). Kesalahpahaman muncul dari "ulangi" saya pikir, seperti halnya: hermanradtke.com/2013/02/06/misunderstanding-dry.html
Marjan Venema
1

Sebenarnya saya ... tidak akan memilih keduanya , tetapi saya akan memilih rekan kerja Anda untuk contoh sederhana Anda.

Hal pertama yang pertama, saya cenderung lebih menyukai inlining nilai daripada melakukan penugasan satu kali. Ini meningkatkan keterbacaan kode bagi saya karena saya tidak harus memecah kereta pemikiran saya menjadi beberapa pernyataan penugasan, kemudian membaca baris kode di mana mereka digunakan. Karena itu, saya akan lebih memilih pendekatan Anda daripada rekan kerja Anda, dengan sedikit nitpick yang testConstructorWithLeadingAndTrailingWhitespaceInName()mungkin saja:

assertThat(new Foo(" " + VALID_NAME + " ").getName(), is(equalTo(VALID_NAME)));

Itu membawa saya ke penamaan. Biasanya, jika tes Anda menyatakan Exceptionakan dilemparkan, nama tes Anda harus menggambarkan perilaku itu juga untuk kejelasan. Menggunakan contoh di atas, jadi bagaimana jika saya memiliki spasi putih tambahan dalam nama ketika saya membangun Fooobjek saya ? Dan bagaimana itu terkait dengan melempar IllegalArgumentException?

Berdasarkan pada nulldan ""tes, saya menduga di sini bahwa hal yang sama Exceptiondilemparkan kali ini untuk menelepon getName(). Jika memang demikian, mungkin menetapkan nama metode pengujian sebagai callingGetNameWithWhitespacePaddingThrows()( IllegalArgumentException- yang saya pikir akan menjadi bagian dari output JUnit, maka opsional) akan lebih baik. Jika memiliki spasi putih dalam nama selama instantiation juga akan membuang yang sama Exception, maka saya akan melewatkan pernyataan sepenuhnya juga, untuk memperjelas bahwa itu dalam perilaku konstruktor.

Namun, saya pikir ada cara yang lebih baik untuk melakukan sesuatu di sini ketika Anda mendapatkan lebih banyak permutasi untuk input yang valid dan tidak valid, yaitu pengujian parameter . Apa yang Anda uji persis seperti itu: Anda membuat banyak Fooobjek dengan argumen konstruktor yang berbeda, dan kemudian menyatakannya gagal pada instantiasi atau pemanggilan getName(). Karena itu, mengapa tidak membiarkan JUnit melakukan iterasi dan perancah untuk Anda? Anda dapat, dalam istilah awam, memberi tahu JUnit, "ini adalah nilai yang ingin saya buat Fooobjeknya", dan kemudian minta metode pengujian Anda menegaskan untukIllegalArgumentExceptiondi tempat yang tepat. Sayangnya, karena cara pengujian parameter JUnit tidak bekerja dengan baik dengan pengujian untuk kasus-kasus yang sukses dan luar biasa dalam pengujian yang sama (sejauh yang saya ketahui), Anda mungkin perlu melompati sedikit lingkaran dengan struktur berikut:

@RunWith(Parameterized.class)
public class FooUnitTests {

    private static final String VALID_NAME = "ABC";

    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                 { VALID_NAME, false }, 
                 { "", true }, 
                 { null, true }, 
                 { " " + VALID_NAME + " ", true }
           });
    }

    private String input;

    private boolean throwException;

    public FooUnitTests(String input, boolean throwException) {
        this.input = input;
        this.throwException = throwException;
    }

    @Test
    public void test() {
        try {
            Foo test = new Foo(input);
            // TODO any testing required for VALID_NAME?
            if (throwException) {
                assertEquals(VALID_NAME, test.getName());
            }
        } catch (IllegalArgumentException e) {
            if (!throwException) {
                throw new RuntimeException(e);
            }
        }
    }
}

Memang, ini terlihat kikuk untuk apa yang dinyatakan sebagai pengujian empat nilai sederhana, dan tidak banyak terbantu dengan implementasi JUnit yang agak membatasi. Dalam hal ini, saya menemukan pendekatan TestNG@DataProvider menjadi lebih berguna, dan itu layak untuk dilihat lebih dekat jika Anda mempertimbangkan tes parameter.

Begini cara seseorang dapat menggunakan TestNG untuk pengujian unit Anda (menggunakan Java 8 Streamuntuk menyederhanakan Iteratorkonstruksi). Beberapa manfaatnya adalah, tetapi tidak terbatas pada:

  1. Parameter uji menjadi argumen metode, bukan bidang kelas.
  2. Anda dapat memiliki beberapa @DataProvideryang menyediakan berbagai jenis input.
    • Ini bisa dibilang lebih jelas dan lebih mudah untuk menambahkan input baru yang valid / tidak valid untuk pengujian.
  3. Ini masih terlihat agak berlebihan untuk contoh sederhana Anda, tetapi IMHO itu lebih baik daripada pendekatan JUnit.
public class FooUnitTests {

    private static final String VALID_NAME = "ABC";

    @DataProvider(name="successes")
    public static Iterator<Object[]> getSuccessCases() {
        return Stream.of(VALID_NAME).map(v -> new Object[]{ v }).iterator();
    }

    @DataProvider(name="exceptions")
    public static Iterator<Object[]> getExceptionCases() {
        return Stream.of("", null, " " + VALID_NAME + " ")
                    .map(v -> new Object[]{ v }).iterator();
    }

    @Test(dataProvider="successes")
    public void testSuccesses(String input) {
        new Foo(input);
        // TODO any further testing required?
    }

    @Test(dataProvider="exceptions", expectedExceptions=IllegalArgumentException.class)
    public void testExceptions(String input) {
        Foo test = new Foo(input);
        if (input.contains(VALID_NAME)) {
            // TestNG favors assert(actual, expected).
            assertEquals(test.getName(), VALID_NAME);
        }
    }
}
hjk
sumber