Cara terbaik untuk unit metode pengujian yang memanggil metode lain di dalam kelas yang sama

35

Saya baru-baru ini berdiskusi dengan beberapa teman yang mana dari 2 metode berikut ini yang terbaik untuk mematikan hasil atau panggilan ke metode di dalam kelas yang sama dari metode di dalam kelas yang sama.

Ini adalah contoh yang sangat sederhana. Pada kenyataannya fungsinya jauh lebih kompleks.

Contoh:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Jadi untuk menguji ini kita punya 2 metode.

Metode 1: Gunakan Fungsi dan Tindakan untuk mengganti fungsionalitas metode. Contoh:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Metode 2: Buat anggota virtual, turunkan kelas dan di kelas turunan menggunakan Fungsi dan Tindakan untuk mengganti fungsionalitas Contoh:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Saya ingin tahu yang lebih baik dan MENGAPA?

Pembaruan: CATATAN: FunctionB juga bisa bersifat publik

tranceru1
sumber
Contoh Anda sederhana, tetapi tidak sepenuhnya benar. FunctionAmengembalikan bool tetapi hanya menetapkan variabel lokal xdan tidak mengembalikan apa pun.
Eric P.
1
Dalam contoh khusus ini, FunctionB bisa public statictetapi di kelas yang berbeda.
Untuk Tinjauan Kode, Anda diharapkan memposting kode aktual bukan versi yang disederhanakan. Lihat FAQ. Sebagai dasarnya, Anda mengajukan pertanyaan spesifik yang tidak mencari tinjauan kode.
Winston Ewert
1
FunctionBrusak oleh desain. new Random().Next()hampir selalu salah. Anda harus menyuntikkan instance dari Random. ( Randomjuga kelas yang dirancang dengan buruk, yang dapat menyebabkan beberapa masalah tambahan)
CodesInChaos
pada catatan yang lebih umum, DI melalui delegasi benar
.

Jawaban:

32

Diedit mengikuti pembaruan poster asli.

Penafian: bukan programmer C # (kebanyakan Java atau Ruby). Jawaban saya adalah: Saya tidak akan mengujinya sama sekali, dan saya pikir Anda tidak harus mengujinya.

Versi yang lebih panjang adalah: metode pribadi / yang dilindungi bukan bagian dari API, mereka pada dasarnya adalah pilihan implementasi, bahwa Anda dapat memutuskan untuk meninjau, memperbarui atau membuang sepenuhnya tanpa dampak apa pun di luar.

Saya kira Anda memiliki tes pada FunctionA (), yang merupakan bagian dari kelas yang terlihat dari dunia eksternal. Ini harus menjadi satu-satunya yang memiliki kontrak untuk diterapkan (dan yang dapat diuji). Metode pribadi Anda / dilindungi tidak memiliki kontrak untuk memenuhi dan / atau menguji.

Lihat diskusi terkait di sana: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Mengikuti komentar , jika FunctionB bersifat publik, saya hanya akan menguji keduanya menggunakan unit test. Anda mungkin berpikir bahwa tes FunctionA tidak sepenuhnya "unit" (seperti yang disebut FunctionB), tapi saya tidak akan terlalu khawatir dengan itu: jika tes FunctionB bekerja tetapi tidak tes FunctionA, itu berarti dengan jelas bahwa masalahnya bukan pada subdomain FunctionB, yang cukup baik bagi saya sebagai pembeda.

Jika Anda benar-benar ingin dapat benar-benar memisahkan dua tes, saya akan menggunakan semacam teknik mengejek untuk mengejek FunctionB saat menguji FunctionA (biasanya, mengembalikan nilai yang benar dikenal tetap). Saya tidak memiliki pengetahuan ekosistem C # untuk memberi saran pada perpustakaan mengejek yang spesifik, tetapi Anda dapat melihat pertanyaan ini .

Martin
sumber
2
Sepenuhnya setuju dengan jawaban @Martin. Saat Anda menulis tes unit untuk kelas Anda tidak harus menguji metode . Apa yang Anda uji adalah perilaku kelas , bahwa kontrak (pernyataan apa yang seharusnya dilakukan kelas) puas. Jadi, tes unit Anda harus mencakup semua persyaratan yang terpapar untuk kelas ini (menggunakan metode / properti publik), termasuk kasus luar biasa
Halo, terima kasih atas jawabannya, tetapi tidak menjawab pertanyaan saya. Saya tidak masalah jika FunctionB bersifat pribadi / dilindungi. Itu juga bisa bersifat publik dan masih bisa dipanggil dari FunctionA.
Cara paling umum untuk menangani masalah ini, tanpa mendesain ulang kelas dasar, adalah dengan subkelas MyClassdan menimpa metode dengan fungsi yang ingin Anda rintisan. Mungkin juga merupakan ide yang baik untuk memperbarui pertanyaan Anda agar FunctionBbisa dimasukkan ke publik.
Eric P.
1
protectedmetode adalah bagian dari permukaan publik suatu kelas, kecuali jika Anda memastikan bahwa tidak ada implementasi kelas Anda di majelis yang berbeda.
CodesInChaos
2
Fakta bahwa FunctionA memanggil FunctionB adalah detail yang tidak relevan dari perspektif pengujian unit. Jika tes untuk FunctionA ditulis dengan benar, itu adalah detail implementasi yang bisa dire-refourored nanti tanpa melanggar tes (selama perilaku keseluruhan FunctionA dibiarkan tidak berubah). Masalah sebenarnya adalah bahwa pengambilan FunctionB dari angka acak perlu dilakukan dengan objek yang disuntikkan, sehingga Anda dapat menggunakan tiruan selama pengujian untuk memastikan bahwa nomor yang terkenal dikembalikan. Ini memungkinkan Anda menguji input / output yang terkenal.
Dan Lyons
11

Saya berlangganan teori bahwa jika suatu fungsi penting untuk diuji, atau penting untuk diganti, cukup penting untuk tidak menjadi detail implementasi pribadi dari kelas yang diuji, tetapi menjadi detail implementasi publik dari suatu kelas yang berbeda .

Jadi jika saya dalam skenario di mana saya miliki

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Lalu saya akan refactor.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Sekarang saya memiliki skenario di mana D () dapat diuji secara independen, dan sepenuhnya dapat diganti.

Sebagai sarana organisasi, kolaborator saya mungkin tidak hidup di tingkat namespace yang sama. Misalnya, jika Aada di FooCorp.BLL, maka kolaborator saya mungkin memiliki lapisan lain, seperti di FooCorp.BLL.Collaborators (atau nama apa pun yang sesuai). Kolaborator saya mungkin hanya akan terlihat di dalam perakitan melalui internalpengubah akses, yang kemudian akan saya paparkan ke proyek pengujian unit saya melalui InternalsVisibleToatribut perakitan. Yang perlu diperhatikan adalah Anda masih bisa menjaga kebersihan API, sejauh yang dipikirkan penelepon, sambil menghasilkan kode yang dapat diverifikasi.

Anthony Pegram
sumber
Ya jika ICollaborator membutuhkan beberapa metode. Jika Anda memiliki objek yang tugasnya hanya membungkus satu metode, saya lebih suka melihatnya diganti dengan delegasi.
jk.
Anda harus memutuskan apakah delegasi bernama masuk akal atau jika antarmuka masuk akal, dan saya tidak akan memutuskan untuk Anda. Secara pribadi, saya tidak menentang kelas metode tunggal (publik). Semakin kecil semakin baik, karena semakin mudah dipahami.
Anthony Pegram
0

Menambahkan pada apa yang Martin tunjukkan,

Jika metode Anda pribadi / dilindungi - jangan mengujinya. Ini internal ke kelas dan tidak boleh diakses di luar kelas.

Dalam kedua pendekatan yang Anda sebutkan, saya memiliki masalah ini -

Metode 1 - Ini benar-benar mengubah kelas yang diuji perilaku dalam tes.

Metode 2 - Ini sebenarnya tidak menguji kode produksi, melainkan menguji implementasi lain.

Dalam masalah yang dinyatakan, saya melihat bahwa satu-satunya logika A adalah untuk melihat apakah output FunctionB adalah genap. Meskipun ilustratif, FunctionB memberikan nilai acak, yang sulit untuk diuji.

Saya mengharapkan skenario realistis di mana kita dapat mengatur MyClass sehingga kita tahu apa yang akan dikembalikan FunctionB. Kemudian hasil yang diharapkan diketahui, kita dapat memanggil FunctionA dan menegaskan hasil yang sebenarnya.

Srikanth Venugopalan
sumber
3
protectedhampir sama dengan public. Hanya privatedan internalmerupakan detail implementasi.
CodesInChaos
@codeinchaos - Saya ingin tahu di sini. Untuk pengujian, metode yang dilindungi adalah 'pribadi' kecuali Anda memodifikasi atribut rakitan. Hanya tipe turunan yang memiliki akses ke anggota yang dilindungi. Dengan pengecualian virtual, saya tidak melihat mengapa dilindungi harus diperlakukan sama dengan publik dari tes. bisakah Anda menjelaskannya?
Srikanth Venugopalan
Karena kelas-kelas turunan itu bisa dalam majelis yang berbeda, mereka terkena kode pihak ketiga, dan dengan demikian bagian dari permukaan publik kelas Anda. Untuk mengujinya, Anda bisa membuatnya internal protected, menggunakan pembantu refleksi pribadi, atau membuat kelas turunan dalam proyek pengujian Anda.
CodesInChaos
@CodesInChaos, setuju bahwa kelas turunan dapat dalam majelis yang berbeda, tetapi ruang lingkupnya masih terbatas pada tipe dasar dan turunan. Memodifikasi pengubah akses hanya agar dapat diuji adalah sesuatu yang membuat saya sedikit gugup. Saya telah melakukannya, tetapi tampaknya menjadi antipernat bagi saya.
Srikanth Venugopalan
0

Saya pribadi menggunakan Method1 yaitu membuat semua metode menjadi Actions atau Funcs karena ini telah sangat meningkatkan testability kode untuk saya. Seperti halnya solusi apa pun ada pro dan kontra dengan pendekatan ini:

Pro

  1. Mengizinkan struktur kode sederhana tempat menggunakan Pola Kode hanya untuk Pengujian Unit dapat menambah kompleksitas tambahan.
  2. Mengizinkan kelas disegel dan untuk penghapusan metode virtual yang diperlukan oleh kerangka kerja mengejek yang populer seperti Moq. Sealing kelas dan menghilangkan metode virtual membuat mereka kandidat untuk inlining dan optimisasi kompiler lainnya. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Menyederhanakan testabilitas sebagai pengganti implementasi Fungsi / Tindakan dalam tes Unit semudah hanya memberikan nilai baru ke Fungsi / Aksi
  4. Juga memungkinkan untuk pengujian jika Fungsi statis dipanggil dari metode lain karena metode statis tidak dapat diejek.
  5. Mudah untuk memperbaiki metode yang ada menjadi Fungsi / Tindakan karena sintaksis untuk memanggil metode di situs panggilan tetap sama. (Untuk saat-saat ketika Anda tidak dapat mengubah metode menjadi Fungsi / Aksi lihat kontra)

Cons

  1. Fungsi / Tindakan tidak dapat digunakan jika kelas Anda dapat diturunkan karena Fungsi / Tindakan tidak memiliki jalur pewarisan seperti Metode
  2. Tidak dapat menggunakan parameter default. Membuat Fungsi dengan parameter standar mencakup membuat delegasi baru yang mungkin membuat kode membingungkan tergantung pada kasus penggunaan
  3. Tidak dapat menggunakan sintaks parameter bernama untuk memanggil metode seperti sesuatu (firstName: "S", lastName: "K")
  4. Con terbesar adalah bahwa Anda tidak memiliki akses ke referensi 'ini' di Fungsi dan Tindakan, dan karenanya setiap dependensi pada kelas harus secara eksplisit diteruskan sebagai parameter. Ini baik karena Anda tahu semua dependensi tetapi buruk jika Anda memiliki banyak properti yang akan bergantung pada Fungsi Anda. Jarak tempuh Anda akan bervariasi berdasarkan pada use case Anda.

Jadi singkatnya, menggunakan Fungsi dan Aksi untuk tes Unit bagus jika Anda tahu bahwa kelas Anda tidak akan pernah ditimpa.

Juga, saya biasanya tidak membuat properti untuk Funcs tetapi sebaris langsung dengannya

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Semoga ini membantu!

Salman Hasrat Khan
sumber
-1

Menggunakan Mock itu mungkin. Nuget: https://www.nuget.org/packages/moq/

Dan percayalah, ini cukup sederhana dan masuk akal.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mock membutuhkan metode virtual untuk menggantikannya.

BrightFuture1029
sumber