Unit Testing di dunia “no setter”

23

Saya tidak menganggap diri saya seorang ahli DDD tetapi, sebagai arsitek solusi, cobalah untuk menerapkan praktik terbaik bila memungkinkan. Saya tahu ada banyak diskusi di sekitar pro dan kontra tentang "gaya" setter no (publik) di DDD dan saya bisa melihat kedua sisi argumen. Masalah saya adalah bahwa saya bekerja pada tim dengan beragam keterampilan, pengetahuan, dan pengalaman yang artinya saya tidak bisa percaya bahwa setiap pengembang akan melakukan hal-hal dengan cara yang "benar". Misalnya, jika objek domain kami dirancang sedemikian rupa sehingga perubahan pada keadaan internal objek dilakukan oleh suatu metode tetapi memberikan setter properti publik, seseorang akan tak terhindarkan mengatur properti alih-alih memanggil metode tersebut. Gunakan contoh ini:

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

Solusi saya adalah membuat setter properti menjadi privat yang dimungkinkan karena ORM yang kami gunakan untuk menghidrasi objek menggunakan refleksi sehingga dapat mengakses seter pribadi. Namun, ini menimbulkan masalah ketika mencoba menulis unit test. Misalnya, ketika saya ingin menulis unit test yang memverifikasi persyaratan yang tidak dapat kami terbitkan kembali, saya perlu menunjukkan bahwa objek tersebut telah diterbitkan. Saya pasti dapat melakukan ini dengan memanggil Terbitkan dua kali, tetapi kemudian pengujian saya mengasumsikan bahwa Publikasi diimplementasikan dengan benar untuk panggilan pertama. Sepertinya agak bau.

Mari buat skenario sedikit lebih nyata dengan kode berikut:

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

Saya ingin menulis unit test yang memverifikasi:

  • Saya tidak dapat menerbitkan kecuali Dokumen telah disetujui
  • Saya tidak dapat menerbitkan kembali Dokumen
  • Saat dipublikasikan, nilai-nilai PublishedBy dan PublishedOn diatur dengan benar
  • Saat dipublikasikan, Publikasi muncul

Tanpa akses ke setter, saya tidak bisa memasukkan objek ke dalam kondisi yang diperlukan untuk melakukan tes. Membuka akses ke setter mengalahkan tujuan mencegah akses.

Bagaimana Anda menyelesaikan (d) masalah ini?

SonOfPirate
sumber
Semakin saya memikirkan hal ini, semakin saya berpikir bahwa seluruh masalah Anda adalah memiliki metode dengan efek samping. Atau lebih tepatnya, objek abadi yang bisa berubah. Dalam dunia DDD, haruskah Anda tidak mengembalikan objek Dokumen baru dari Approve dan Publikasikan, daripada memperbarui keadaan internal objek ini?
pdr
1
Pertanyaan cepat, O / RM mana yang Anda gunakan. Saya penggemar berat EF tetapi mendeklarasikan setter sebagai terlindungi sedikit menggosok saya dengan cara yang salah.
Michael Brown
Kami memiliki campuran sekarang karena pengembangan rentang bebas yang telah saya tuduh. Beberapa ADO.NET menggunakan AutoMapper untuk menghidrasi dari DataReader, beberapa model Linq-to-SQL (yang akan menjadi pengganti berikutnya ) dan beberapa model EF baru.
SonOfPirate
Memanggil Publikasi dua kali tidak berbau sama sekali dan merupakan cara untuk melakukannya.
Piotr Perak

Jawaban:

27

Saya tidak bisa memasukkan objek ke dalam kondisi yang diperlukan untuk melakukan tes.

Jika Anda tidak dapat menempatkan objek ke dalam negara yang dibutuhkan untuk melakukan tes, maka Anda tidak dapat menempatkan objek ke dalam negara dalam kode produksi, sehingga tidak perlu untuk menguji bahwa negara. Jelas, ini tidak benar dalam kasus Anda, Anda dapat menempatkan objek Anda ke dalam kondisi yang diperlukan, panggil saja Menyetujui.

  • Saya tidak dapat mempublikasikan kecuali jika Dokumen telah disetujui: tulis tes yang memanggil publikasikan sebelum menelepon menyetujui menyebabkan kesalahan yang tepat tanpa mengubah status objek.

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • Saya tidak dapat menerbitkan kembali Dokumen: tulis tes yang menyetujui objek, lalu panggil terbitkan setelah berhasil, tetapi yang kedua kali menyebabkan kesalahan yang tepat tanpa mengubah status objek.

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • Saat dipublikasikan, nilai-nilai PublishedBy dan PublishedOn diatur dengan benar: tulis tes yang disetujui panggilan lalu panggil terbitkan, menyatakan bahwa status objek berubah dengan benar

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • Saat dipublikasikan, diterbitkanEvent dinaikkan: kaitkan ke sistem acara dan atur bendera untuk memastikan namanya

Anda juga perlu menulis tes untuk menyetujui.

Dengan kata lain, jangan menguji hubungan antara bidang internal dan IsPublished dan IsApproved, tes Anda akan sangat rapuh jika Anda melakukan itu karena mengubah bidang Anda berarti mengubah kode tes Anda, sehingga tes itu akan cukup sia-sia. Alih-alih, Anda harus menguji hubungan antara panggilan metode publik, dengan cara ini, bahkan jika Anda memodifikasi bidang Anda tidak perlu memodifikasi tes.

Lie Ryan
sumber
Ketika Menyetujui istirahat, beberapa tes rusak. Anda tidak lagi menguji unit kode, Anda menguji implementasi penuh.
pdr
Saya berbagi keprihatinan pdr, itulah sebabnya saya ragu-ragu untuk pergi ke arah ini. Ya, sepertinya terbersih, tetapi saya tidak suka memiliki banyak alasan, tes individual dapat gagal.
SonOfPirate
4
Saya belum melihat unit test yang hanya bisa gagal karena satu kemungkinan alasan. Juga, Anda dapat menempatkan bagian "manipulasi negara" dari tes ke dalam setup()metode --- bukan tes itu sendiri.
Peter K.
12
Mengapa tergantung pada approve()entah bagaimana rapuh, namun tergantung pada setApproved(true)entah bagaimana tidak? approve()adalah ketergantungan yang sah dalam pengujian karena merupakan ketergantungan pada persyaratan. Jika ketergantungan hanya ada dalam tes, itu akan menjadi masalah lain.
Karl Bielefeldt
2
@ pdr, bagaimana Anda menguji kelas stack? Apakah Anda akan mencoba untuk menguji push()dan pop()metode secara mandiri?
Winston Ewert
2

Namun pendekatan lain adalah membuat konstruktor kelas yang memungkinkan properti internal diatur pada instantiation:

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}
Peter K.
sumber
2
Ini tidak skala sama sekali. Objek saya dapat memiliki lebih banyak properti dengan sejumlah di antaranya memiliki atau tidak memiliki nilai pada titik tertentu dalam siklus hidup objek. Saya mengikuti prinsip bahwa konstruktor berisi parameter untuk properti yang diperlukan agar objek berada dalam keadaan awal yang valid atau dependensi yang dibutuhkan suatu objek agar berfungsi. Tujuan dari properti dalam contoh adalah untuk menangkap keadaan saat ini ketika objek dimanipulasi. Memiliki konstruktor dengan setiap properti atau kelebihan dengan kombinasi yang berbeda adalah bau besar dan, seperti yang saya katakan, tidak berskala.
SonOfPirate
Dimengerti. Contoh Anda tidak menyebutkan lebih banyak properti, dan nomor dalam contoh adalah "pada puncak" memiliki ini sebagai pendekatan yang valid. Tampaknya ini memberi tahu Anda sesuatu tentang desain Anda: Anda tidak dapat menempatkan objek Anda ke keadaan valid apa pun di Instantiation. Itu berarti Anda harus memasukkannya ke dalam kondisi awal yang valid, dan mereka memanipulasinya ke dalam kondisi yang tepat untuk pengujian. Itu menyiratkan jawaban Lie Ryan adalah jalan yang harus ditempuh .
Peter K.
Bahkan jika objek memiliki satu properti dan tidak akan pernah mengubah solusi ini buruk. Apa yang menghentikan siapa pun menggunakan konstruktor ini dalam produksi? Bagaimana Anda menandai konstruktor ini [TestOnly]?
Piotr Perak
Mengapa produksi buruk? (Sungguh, saya ingin tahu). Kadang-kadang perlu untuk menciptakan kembali keadaan yang tepat dari suatu objek saat membuat ... bukan hanya objek awal yang valid.
Peter K.
1
Jadi sementara itu membantu menempatkan objek ke dalam keadaan awal yang valid, menguji perilaku objek di dalamnya berlangsung melalui siklus hidupnya mensyaratkan bahwa objek diubah dari keadaan awal. OP saya harus melakukan pengujian status tambahan ini ketika Anda tidak bisa begitu saja mengatur properti untuk mengubah status objek.
SonOfPirate
1

Salah satu strategi adalah bahwa Anda mewarisi kelas (dalam hal ini Dokumen) dan menulis tes terhadap kelas yang diwarisi. Kelas yang diwariskan memungkinkan beberapa cara untuk mengatur keadaan objek dalam tes.

Dalam C # satu strategi bisa membuat setter internal, kemudian mengekspos internal untuk menguji proyek.

Anda juga dapat menggunakan API kelas seperti yang Anda gambarkan ("Saya pasti bisa melakukan ini dengan memanggil Terbitkan dua kali"). Ini akan menetapkan status objek menggunakan layanan publik objek, sepertinya tidak terlalu bau bagi saya. Dalam contoh Anda, ini mungkin akan menjadi cara saya melakukannya.

simoraman
sumber
Saya berpikir tentang ini sebagai solusi yang mungkin tetapi ragu-ragu untuk membuat properti saya dapat ditimpa atau mengekspos setter sebagai dilindungi karena rasanya seperti saya membuka objek dan memecahkan enkapsulasi. Saya pikir membuat properti terlindungi tentu lebih baik daripada publik atau bahkan internal / teman. Saya pasti akan memberikan pendekatan ini lebih banyak pemikiran. Sederhana dan efektif. Terkadang itulah pendekatan terbaik. Jika ada yang tidak setuju, silakan tambahkan komentar dengan spesifik.
SonOfPirate
1

Untuk menguji dalam isolasi absolut perintah dan kueri yang diterima objek domain, saya terbiasa menyediakan setiap tes dengan serialisasi objek dalam keadaan yang diharapkan. Di bagian susunan tes, ini memuat objek untuk menguji dari file yang telah saya siapkan sebelumnya. Pada awalnya saya mulai dengan serialisasi biner, tetapi json terbukti jauh lebih mudah untuk dipertahankan. Ini terbukti bekerja dengan baik, setiap kali isolasi absolut dalam tes memberikan nilai aktual.

sunting hanya sebuah catatan, beberapa kali serialisasi JSON gagal (seperti dalam kasus grafik objek siklik, yang berbau, btw). Dalam situasi seperti itu, saya menyelamatkan serialisasi biner. Agak pragmatis, tetapi berhasil. :-)

Giacomo Tesio
sumber
Dan bagaimana Anda mempersiapkan objek dalam kondisi yang diharapkan jika tidak ada setter dan Anda tidak ingin menyebutnya metode publik untuk mengaturnya?
Piotr Perak
Saya telah menulis alat kecil untuk itu. Ini memuat kelas dengan refleksi yang membuat entitas baru menggunakan konstruktor publik itu (biasanya mengambil hanya pengidentifikasi) dan memanggil array Aksi <TEntity> di atasnya, menyimpan snapshot setelah setiap operasi (dengan nama konvensional berdasarkan indeks aksi itu dan nama entitas). Alat dieksekusi secara manual di setiap refactoring kode entitas dan snapshot dilacak oleh DCVS. Jelas setiap tindakan memanggil perintah publik dari entitas, tetapi ini dilakukan dari tes berjalan bahwa cara ini benar-benar tes unit .
Giacomo Tesio
Saya tidak mengerti bagaimana itu mengubah apa pun. Jika masih memanggil metode publik pada sut (sistem yang diuji) maka tidak ada bedanya maka hanya memanggil metode-metode tersebut dalam pengujian.
Piotr Perak
Setelah foto dibuat, mereka disimpan dalam file. Setiap tes tidak tergantung pada urutan operasi yang diperlukan untuk mendapatkan status awal entitas, tetapi dari status itu sendiri (diambil dari snapshot). Metode yang diuji sendiri kemudian diisolasi dari perubahan ke metode lain.
Giacomo Tesio
Bagaimana ketika seseorang mengubah metode publik yang digunakan untuk menyiapkan keadaan berseri untuk pengujian Anda tetapi lupa menjalankan alat untuk membuat ulang objek berseri? Tes masih hijau meskipun ada kesalahan dalam kode. Tetap saya katakan ini tidak mengubah apa pun. Anda masih menjalankan metode publik sehingga mengatur objek yang Anda uji. Tapi Anda menjalankannya jauh sebelum tes dijalankan.
Piotr Perak
-7

Kamu bilang

cobalah untuk menerapkan praktik terbaik bila memungkinkan

dan

ORM yang kami gunakan untuk menghidrasi objek menggunakan refleksi sehingga dapat mengakses setter pribadi

dan saya harus berpikir bahwa menggunakan refleksi untuk memintas kontrol akses di kelas Anda bukan apa yang saya gambarkan sebagai "praktik terbaik". Ini akan menjadi sangat lambat juga.


Secara pribadi, saya akan memo kerangka unit test Anda dan pergi dengan sesuatu di kelas - tampaknya Anda sedang menulis tes dari sudut pandang menguji seluruh kelas, yang bagus. Di masa lalu, untuk beberapa komponen rumit yang memerlukan pengujian, saya ave memasukkan kode pengaturan dan pengaturan ke dalam kelas itu sendiri (dulu merupakan pola desain umum untuk memiliki metode pengujian () di setiap kelas), sehingga Anda membuat klien yang hanya instantiate objek dan memanggil metode pengujian yang dapat mengatur dirinya sendiri sesuka Anda tanpa nastiness seperti hacks refleksi.

Jika Anda khawatir tentang kode mengasapi, cukup bungkus metode pengujian di #ifdefs agar hanya tersedia dalam kode debug (mungkin praktik terbaik itu sendiri)

gbjbaanb
sumber
4
-1: Memotong kerangka pengujian Anda dan kembali ke metode pengujian di dalam kelas akan kembali ke zaman kegelapan pengujian unit.
Robert Johnson
9
Tidak -1 dari saya, tetapi termasuk kode uji dalam produksi umumnya adalah Bad Thing (TM) .
Peter K.
apa lagi yang OP lakukan? Tetap mengacau dengan setter pribadi ?! Ini seperti memilih racun mana yang ingin Anda minum. Saran saya kepada OP adalah untuk menempatkan unit test ke dalam kode debug, bukan produksi. Dalam pengalaman saya, menempatkan unit test ke proyek yang berbeda hanya berarti bahwa proyek terkait erat dengan aslinya, jadi dari PoV dev, ada sedikit perbedaan.
gbjbaanb