Apakah menggunakan unit test untuk menceritakan sebuah cerita adalah ide yang bagus?

13

Jadi, saya punya modul otentikasi yang saya tulis beberapa waktu lalu. Sekarang saya melihat kesalahan cara saya dan menulis tes unit untuk itu. Saat menulis unit test, saya kesulitan menemukan nama baik dan area bagus untuk diuji. Misalnya, saya punya hal-hal seperti

  • MembutuhkanLogin_should_redirect_when_not_logged_in
  • MembutuhkanLogin_should_pass_through_when_logged_in
  • Login_should_work_when_given_proper_credentials

Secara pribadi, saya pikir itu agak jelek, meskipun sepertinya "tepat". Saya juga mengalami kesulitan membedakan antara tes hanya dengan memindai mereka (saya harus membaca nama metode setidaknya dua kali untuk mengetahui apa yang baru saja gagal)

Jadi, saya berpikir bahwa mungkin alih-alih menulis tes yang murni menguji fungsionalitas, mungkin menulis serangkaian tes yang mencakup skenario.

Misalnya, ini adalah rintisan tes yang saya buat:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

Apakah ini pola yang terdengar? Saya telah melihat kisah penerimaan dan semacamnya, tetapi ini pada dasarnya berbeda. Perbedaan besar adalah bahwa saya datang dengan skenario untuk "memaksa" tes. Daripada mencoba secara manual untuk menemukan kemungkinan interaksi yang perlu saya uji. Juga, saya tahu ini mendorong unit test yang tidak menguji persis satu metode dan kelas. Saya pikir ini tidak masalah. Juga, saya sadar bahwa ini akan menyebabkan masalah untuk setidaknya beberapa kerangka kerja pengujian, karena mereka biasanya menganggap bahwa pengujian independen satu sama lain dan urutan tidak masalah (di mana dalam kasus ini).

Ngomong-ngomong, apakah ini pola yang disarankan? Atau, apakah ini akan sangat cocok untuk tes integrasi API saya daripada sebagai tes "unit"? Ini hanya dalam proyek pribadi, jadi saya terbuka untuk eksperimen yang mungkin atau mungkin tidak berjalan dengan baik.

Earlz
sumber
4
Garis antara unit, tes integrasi dan fungsional kabur, jika saya harus memilih nama untuk rintisan pengujian Anda, itu akan fungsional.
yannis
Saya pikir ini masalah selera. Secara pribadi saya menggunakan nama apa pun yang saya uji dengan _testmenambahkan dan menggunakan komentar untuk mencatat hasil apa yang saya harapkan. Jika ini adalah proyek pribadi, temukan beberapa gaya yang Anda rasa nyaman dan pertahankan.
Tn. Lister
1
Saya telah menulis jawaban dengan perincian tentang cara menulis unit test yang lebih tradisional menggunakan pola Arrange / Act / Assert, tetapi seorang teman telah banyak sukses menggunakan github.com/cucumber/cucumber/wiki/Gherkin , yang merupakan digunakan untuk spesifikasi dan afaik dapat menghasilkan tes mentimun.
StuperUser
Meskipun saya tidak akan menggunakan metode yang telah Anda tunjukkan dengan nunit atau serupa, nspec memiliki dukungan untuk membangun konteks dan pengujian dalam sifat yang lebih didorong oleh cerita: nspec.org
Mike
1
ubah "Bill" menjadi "User" dan Anda selesai
Steven A. Lowe

Jawaban:

15

Ya, sebaiknya berikan nama pengujian Anda pada contoh skenario yang Anda uji. Dan menggunakan alat pengujian unit Anda untuk lebih dari sekedar pengujian unit mungkin ok juga, banyak orang melakukan ini dengan sukses (saya juga).

Tapi tidak, itu jelas bukan ide yang baik untuk menulis tes Anda dengan cara di mana urutan pelaksanaan tes itu penting. Misalnya, NUnit memungkinkan pengguna untuk memilih secara interaktif tes mana yang ingin dieksekusi, jadi ini tidak akan berfungsi seperti yang dimaksudkan lagi.

Anda dapat menghindari ini dengan mudah di sini dengan memisahkan bagian pengujian utama dari setiap pengujian (termasuk "menegaskan") dari bagian-bagian yang mengatur sistem Anda dalam keadaan awal yang benar. Menggunakan contoh Anda di atas: tulis metode untuk membuat akun, masuk dan kirim komentar - tanpa ada pernyataan. Kemudian gunakan kembali metode tersebut dalam tes yang berbeda. Anda juga harus menambahkan beberapa kode ke [Setup]metode perlengkapan pengujian Anda untuk memastikan sistem dalam keadaan awal yang didefinisikan dengan benar (misalnya, tidak ada akun sejauh ini dalam database, tidak ada yang terhubung sejauh ini dll).

EDIT: Tentu saja, ini tampaknya bertentangan dengan sifat "cerita" dari tes Anda, tetapi jika Anda memberikan metode yang berarti pada helper Anda, Anda menemukan cerita Anda dalam setiap tes.

Jadi, akan terlihat seperti ini:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}
Doc Brown
sumber
Saya akan melangkah lebih jauh dan mengatakan bahwa menggunakan alat xUnit untuk menjalankan tes fungsional baik-baik saja, asalkan Anda tidak membingungkan alat dengan jenis tes, dan Anda menjaga tes ini terpisah dari tes unit yang sebenarnya, sehingga dev dapat masih menjalankan tes unit dengan cepat pada waktu komit. Ini cenderung jauh lebih lambat daripada tes unit.
bdsl
4

Masalah dengan menceritakan sebuah cerita dengan tes unit adalah bahwa tidak membuat eksplisit bahwa tes unit harus diatur dan dijalankan sepenuhnya secara independen satu sama lain.

Tes unit yang baik harus sepenuhnya diisolasi dari semua kode dependen lainnya, ini adalah unit kode terkecil yang dapat diuji.

Ini memberikan manfaat yang sama halnya dengan mengkonfirmasi kode bekerja, jika tes gagal Anda mendapatkan diagnosis di mana kode salah secara gratis. Jika suatu tes tidak terisolasi, Anda harus melihat pada apa itu tergantung untuk mencari tahu apa yang salah dan kehilangan manfaat utama dari pengujian unit. Memiliki urutan masalah eksekusi juga dapat meningkatkan banyak negatif palsu, jika tes gagal, mungkin tes berikut gagal meskipun kode yang mereka uji berfungsi dengan baik.

Artikel bagus yang lebih mendalam adalah klasik tentang tes hybrid kotor .

Untuk membuat kelas, metode, dan hasil dapat dibaca, pengujian Seni Unit yang hebat menggunakan konvensi penamaan

Kelas Tes:

Uji KelasTest

Metode tes:

MethodUnderTest_Condition_ExpectedResult

Untuk menyalin contoh @Doc Brown, daripada menggunakan [Setup] yang berjalan sebelum setiap tes, saya menulis metode pembantu untuk membangun objek terisolasi untuk menguji.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Jadi tes gagal memiliki nama yang bermakna yang memberi Anda beberapa narasi tentang metode apa yang gagal, kondisi dan hasil yang diharapkan.

Begitulah cara saya selalu menulis unit test, tetapi seorang teman telah banyak sukses dengan Gerkin .

StuperUser
sumber
1
Meskipun saya pikir ini adalah posting yang bagus, saya tidak setuju dengan apa yang dikatakan artikel terkait tentang tes "hybrid". Memiliki tes integrasi "kecil" (selain itu, tidak sebagai alternatif untuk tes unit murni , tentu saja) dapat IMHO sangat membantu, bahkan jika mereka tidak dapat memberi tahu Anda dengan tepat metode mana yang berisi kode yang salah. Jika tes-tes tersebut dapat dipertahankan tergantung pada seberapa bersih kode untuk tes-tes tersebut ditulis, mereka tidak "kotor" per se. Dan saya pikir tujuan dari tes itu bisa sangat jelas (seperti pada contoh OP).
Doc Brown
3

Apa yang Anda gambarkan terdengar lebih seperti Behavior Driven Design (BDD) daripada pengujian unit kepada saya. Lihatlah SpecFlow yang merupakan teknologi .NET BDD yang didasarkan pada Gherkin DSL.

Hal-hal kuat yang dapat dibaca / ditulis manusia mana pun tanpa mengetahui apa pun tentang pengkodean. Tim uji kami menikmati kesuksesan besar memanfaatkannya untuk suite uji integrasi kami.

Mengenai konvensi untuk unit test, jawaban @ DocBrown tampaknya solid.

Drew Marsh
sumber
Sebagai informasi, BDD persis seperti TDD, hanya gaya penulisan yang berubah. Contoh: TDD = assert(value === expected)BDD = value.should.equals(expected)+ Anda menggambarkan fitur dalam lapisan yang memecahkan masalah "unit test independent". Ini gaya yang hebat!
Offirmo