Unit Fairy Magic Fairy: DSL dalam C #

Seberapa sering hal itu terjadi ketika Anda menulis tes unit kerja, Anda melihat kodenya, dan apakah itu ... buruk? Dan Anda berpikir seperti itu: "Ini ujian, mari kita biarkan seperti itu ...". Tidak,% nama pengguna%, jadi jangan tinggalkan. Pengujian adalah bagian penting dari sistem yang menyediakan dukungan kode, dan sangat penting bahwa bagian ini juga didukung. Sayangnya, kami tidak memiliki banyak cara untuk memastikan hal ini (kami tidak akan menulis tes untuk tes), tetapi masih ada pasangan.


Di sekolah pengembang Dodo DevSchool kami, kami menyoroti, antara lain, kriteria berikut untuk ujian yang baik:

  • reproduktifitas: menjalankan tes pada kode dan input yang sama selalu mengarah pada hasil yang sama;
  • fokus: seharusnya hanya ada satu alasan untuk tes jatuh;
  • dimengerti: baik, ini dia jelas. :)

Bagaimana Anda menyukai tes seperti itu dalam hal kriteria ini?

[Fact] public void AcceptOrder_Successful() { var ingredient1 = new Ingredient("Ingredient1"); var ingredient2 = new Ingredient("Ingredient2"); var ingredient3 = new Ingredient("Ingredient3"); var order = new Order(DateTime.Now); var product1 = new Product("Pizza1"); product1.AddIngredient(ingredient1); product1.AddIngredient(ingredient2); var orderLine1 = new OrderLine(product1, 1, 500); order.AddLine(orderLine1); var product2 = new Product("Pizza2"); product2.AddIngredient(ingredient1); product2.AddIngredient(ingredient3); var orderLine2 = new OrderLine(product2, 1, 650); order.AddLine(orderLine2); var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); var service = new PizzeriaService(orderRepositoryMock.Object, ingredientsRepositoryMock.Object); service.AcceptOrder(order); orderRepositoryMock.Verify(r => r.Add(order), Times.Once); ingredientsRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } 

Bagi saya - sangat buruk.

Itu tidak bisa dipahami: misalnya, saya bahkan tidak bisa mengalokasikan blok Atur, Tindakan, dan Tegas.

Tidak dapat diputar: Properti DateTime.Now digunakan. Dan akhirnya, itu tidak fokus, karena memiliki 2 alasan untuk musim gugur: panggilan ke metode dua repositori diperiksa.

Selain itu, meskipun penamaan tes berada di luar cakupan artikel ini, saya masih memperhatikan namanya: dengan seperangkat sifat negatif, sulit untuk merumuskannya sehingga ketika melihat nama tes, orang luar segera mengerti mengapa tes ini umumnya dalam proyek.
Jika Anda tidak dapat memberi nama tes secara ringkas, maka ada yang salah dengan tes tersebut.
Karena tes ini tidak dapat dipahami, beri tahu Anda apa yang terjadi di dalamnya:

  1. Bahan dibuat.
  2. Dari bahan, produk (pizza) dibuat.
  3. Pesanan dibuat dari produk.
  4. Layanan dibuat untuk repositori basah.
  5. Pesanan diteruskan ke metode layanan AcceptOrder.
  6. Telah diverifikasi bahwa metode Tambah dan Cadangan Bahan repositori masing-masing telah dipanggil.

Jadi bagaimana kita membuat tes ini lebih baik? Anda perlu mencoba untuk meninggalkan dalam tes hanya apa yang benar-benar penting. Dan untuk itu, orang pintar seperti Martin Fowler dan Rebecca Parsons datang dengan DSL (Domain Specific Language) . Di sini saya akan berbicara tentang pola DSL yang kami gunakan di Dodo untuk memastikan bahwa unit test kami lunak dan halus, dan pengembang merasa percaya diri setiap hari.

Rencananya adalah ini: pertama kita akan membuat tes ini dapat dimengerti, kemudian kita akan bekerja pada kemampuan reproduksi dan berakhir dengan membuatnya fokus. Kami melaju ...

Pembuangan bahan (objek domain yang ditentukan sebelumnya)


Mari kita mulai dengan blok pembuatan pesanan. Memesan adalah salah satu entitas domain pusat. Akan lebih keren jika kita bisa menggambarkan urutan sedemikian rupa sehingga bahkan orang yang tidak tahu cara menulis kode tetapi memahami logika domain dapat memahami jenis pesanan yang kita buat. Untuk melakukan ini, pertama-tama, kita harus meninggalkan penggunaan abstrak "Ingredient1" dan "Pizza1" menggantikan mereka dengan bahan-bahan nyata, pizza dan objek domain lainnya.

Kandidat pertama untuk optimasi adalah bahan. Semuanya sederhana dengan mereka: mereka tidak memerlukan penyesuaian, hanya panggilan ke konstruktor. Cukup membawa mereka ke dalam wadah terpisah dan menamainya sehingga jelas bagi para pakar domain:

 public static class Ingredients { public static readonly Ingredient Dough = new Ingredient("Dough"); public static readonly Ingredient Pepperoni = new Ingredient("Pepperoni"); public static readonly Ingredient Mozzarella = new Ingredient("Mozzarella"); } 

Alih-alih Ingredient1 yang benar-benar gila, Ingredient2 dan Ingredient3, kami mendapatkan Dough, Pepperoni dan Mozzarella.
Gunakan objek domain yang telah ditentukan sebelumnya untuk entitas domain yang umum digunakan.

Builder untuk produk


Entitas domain berikutnya adalah produk. Semuanya sedikit lebih rumit dengan mereka: setiap produk terdiri dari beberapa bahan dan kami harus menambahkannya ke produk sebelum digunakan.

Di sini, pola Builder tua yang baik berguna. Ini adalah versi build saya untuk produk:

 public class ProductBuilder { private Product _product; public ProductBuilder(string name) { _product = new Product(name); } public ProductBuilder Containing(Ingredient ingredient) { _product.AddIngredient(ingredient); return this; } public Product Please() { return _product; } } 

Ini terdiri dari konstruktor parameter, metode Containing kustom, dan metode terminal Please . Jika Anda tidak suka berbaik hati dengan kode tersebut, maka Anda dapat mengganti Please with Now . Builder menyembunyikan konstruktor kompleks dan pemanggilan metode yang mengkonfigurasi objek. Kode menjadi lebih bersih dan lebih mudah dimengerti. Dengan cara yang baik, pembangun harus menyederhanakan pembuatan objek sehingga kode jelas bagi pakar domain. Sangat layak menggunakan pembangun untuk objek yang memerlukan konfigurasi sebelum mulai bekerja.

Pembangun produk akan memungkinkan Anda untuk membuat desain seperti:

 var pepperoni = new ProductBuilder("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); 

Builds membantu Anda membuat objek yang perlu dikustomisasi. Pertimbangkan untuk membuat builder walaupun konfigurasi terdiri dari satu baris.

ObjectMother


Terlepas dari kenyataan bahwa penciptaan produk telah menjadi jauh lebih baik, perancang new ProductBuilder masih terlihat sangat jelek. Perbaiki dengan pola ObjectMother (Father).

Polanya sederhana seperti 5 kopecks: kami membuat kelas statis dan mengumpulkan semua pembangun ke dalamnya.

 public static class Create { public static ProductBuilder Product(string name) => new ProductBuilder(name); } 

Sekarang Anda dapat menulis seperti ini:

 var pepperoni = Create.Product("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); 

ObjectMother diciptakan untuk pembuatan objek deklaratif. Selain itu, membantu untuk memperkenalkan pengembang baru ke dalam domain, sebagai saat menulis kata Create IDE sendiri akan memberi tahu Anda apa yang dapat Anda buat di domain ini.

Dalam kode kami, ObjectMother kadang-kadang disebut Bukan Create , tapi Given . Saya suka kedua opsi. Jika Anda punya ide lain - bagikan di komentar.
Untuk secara deklaratif membuat objek, gunakan ObjectMother. Kode akan menjadi lebih bersih, dan akan lebih mudah bagi pengembang baru untuk mempelajari domain.

Penghapusan produk


Sudah menjadi jauh lebih baik, tetapi produk masih memiliki ruang untuk tumbuh. Kami memiliki sejumlah produk dan, seperti bahan, mereka dapat dikumpulkan dalam kelas terpisah dan tidak diinisialisasi untuk setiap tes:

 public static class Pizza { public static Product Pepperoni => Create.Product("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); public static Product Margarita => Create.Product("Margarita") .Containing(Ingredients.Dough) .Containing(Ingredients.Mozzarella) .Please(); } 

Di sini saya menyebut wadah bukan Products , melainkan Pizza . Nama ini membantu membaca tes. Misalnya, ada baiknya Anda menghilangkan pertanyaan seperti “Apakah Pepperoni pizza atau sosis?”.
Cobalah untuk menggunakan objek domain nyata, bukan pengganti seperti Product1.

Pembangun untuk pesanan (contoh dari belakang)


Sekarang kita menerapkan pola yang dijelaskan untuk membuat pembangun pesanan, tapi sekarang mari kita pergi bukan dari pembangun, tetapi dari apa yang ingin kita terima. Ini adalah bagaimana saya ingin membuat pesanan:

 var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); 

Bagaimana kita bisa mencapai ini? Kami jelas membutuhkan pembangun untuk pesanan dan jalur pemesanan. Dengan pembangun untuk memesan semuanya jernih. Ini dia:

 public class OrderBuilder { private DateTime _date; private readonly List<OrderLine> _lines = new List<OrderLine>(); public OrderBuilder Dated(DateTime date) { _date = date; return this; } public OrderBuilder With(OrderLine orderLine) { _lines.Add(orderLine); return this; } public Order Please() { var order = new Order(_date); foreach (var line in _lines) { order.AddLine(line); } return order; } } 

Tetapi dengan OrderLine situasinya lebih menarik: pertama, terminal Tolong metode tidak dipanggil di sini, dan kedua, akses ke pembangun disediakan bukan oleh Create statis dan bukan konstruktor pembangun itu sendiri. Kami akan memecahkan masalah pertama menggunakan implicit operator dan pembangun kami akan terlihat seperti ini:

 public class OrderLineBuilder { private Product _product; private decimal _count; private decimal _price; public OrderLineBuilder Of(decimal count, Product product) { _product = product; _count = count; return this; } public OrderLineBuilder For(decimal price) { _price = price; return this; } public static implicit operator OrderLine(OrderLineBuilder b) { return new OrderLine(b._product, b._count, b._price); } } 

Metode kedua akan membantu kami memahami metode Ekstensi untuk kelas Product :

 public static class ProductExtensions { public static OrderLineBuilder CountOf(this Product product, decimal count) { return Create.OrderLine.Of(count, product) } } 

Secara umum, metode ekstensi adalah teman baik DSL. Mereka dapat membuat deskripsi yang deklaratif dan dapat dimengerti dari logika yang sepenuhnya jahat.
Gunakan metode ekstensi. Gunakan saja. :)
Setelah melakukan semua tindakan ini, kami mendapatkan kode tes berikut:

 [Fact] public void AcceptOrder_Successful() { var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); var service = new PizzeriaService(orderRepositoryMock.Object, ingredientsRepositoryMock.Object); service.AcceptOrder(order); orderRepositoryMock.Verify(r => r.Add(order), Times.Once); ingredientsRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } 

Di sini kita telah mengambil pendekatan yang kita sebut "Peri Peri". Ini adalah saat Anda pertama kali menulis kode siaga seperti yang Anda ingin melihatnya, dan kemudian mencoba untuk membungkus apa yang Anda tulis dalam DSL. Ini sangat berguna untuk bertindak - kadang-kadang Anda sendiri tidak dapat membayangkan apa yang mampu dilakukan oleh C #.
Bayangkan bahwa peri ajaib telah tiba dan memungkinkan Anda untuk menulis kode seperti yang Anda inginkan, dan kemudian mencoba untuk membungkus semua yang ditulis dalam DSL.

Menciptakan layanan (pola yang dapat diuji)


Dengan pesanan sekarang semuanya kurang lebih tidak buruk. Waktunya telah tiba untuk berurusan dengan mokas dari repositori. Layak dikatakan di sini bahwa tes itu sendiri, yang kami pertimbangkan, adalah tes untuk perilaku. Tes perilaku sangat terkait dengan penerapan metode, dan jika mungkin untuk tidak menulis tes seperti itu, maka lebih baik tidak melakukannya. Namun, terkadang mereka berguna, dan kadang-kadang, Anda tidak dapat melakukannya tanpa mereka sama sekali. Teknik berikut ini membantu untuk menulis dengan tepat tes untuk perilaku, dan jika Anda tiba-tiba menyadari bahwa Anda ingin menggunakannya, pertama-tama pikirkan apakah Anda dapat menulis ulang tes sedemikian rupa sehingga mereka memeriksa keadaan, bukan perilaku.

Jadi, saya ingin memastikan bahwa dalam metode pengujian saya tidak ada satu pel. Untuk melakukan ini, saya akan membuat pembungkus untuk PizzeriaService di mana saya merangkum semua logika yang memeriksa panggilan metode:

 public class PizzeriaServiceTestable : PizzeriaService { private readonly Mock<IOrderRepository> _orderRepositoryMock; private readonly Mock<IIngredientRepository> _ingredientRepositoryMock; public PizzeriaServiceTestable(Mock<IOrderRepository> orderRepositoryMock, Mock<IIngredientRepository> ingredientRepositoryMock) : base(orderRepositoryMock.Object, ingredientRepositoryMock.Object) { _orderRepositoryMock = orderRepositoryMock; _ingredientRepositoryMock = ingredientRepositoryMock; } public void VerifyAddWasCalledWith(Order order) { _orderRepositoryMock.Verify(r => r.Add(order), Times.Once); } public void VerifyReserveIngredientsWasCalledWith(Order order) { _ingredientRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } } 

Kelas ini akan memungkinkan kita untuk memeriksa pemanggilan metode, tetapi kita masih perlu membuatnya entah bagaimana. Untuk melakukan ini, kita akan menggunakan pembuat yang sudah kita ketahui:

 public class PizzeriaServiceBuilder { public PizzeriaServiceTestable Please() { var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); return new PizzeriaServiceTestable(orderRepositoryMock, ingredientsRepositoryMock); } } 

Saat ini, metode pengujian kami terlihat seperti ini:

 [Fact] public void AcceptOrder_Successful() { var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyAddWasCalledWith(order); service.VerifyReserveIngredientsWasCalledWith(order); } 

Metode panggilan pengujian bukan satu-satunya alasan kelas Testable dapat digunakan. Di sini, misalnya, di sini Dima Pavlov kami menggunakannya untuk refactoring kode legacy yang kompleks.
Dapat diuji mampu menyelamatkan situasi dalam kasus yang paling sulit. Untuk tes perilaku, ini membantu untuk membungkus cek panggilan jelek menjadi metode yang indah.
Pada saat yang penting ini, kami selesai untuk memahami kemampuan ujian. Tetap membuatnya reproduktif dan fokus.

Reproducibilitas (Ekstensi Literal)


Pola Perluasan Literal tidak secara langsung terkait dengan reproduktifitas, tetapi itu akan membantu kita. Masalah kami saat ini adalah kami menggunakan DateTime.Now sebagai tanggal pesanan. Jika tiba-tiba mulai dari tanggal tertentu, logika penerimaan pesanan berubah, maka dalam logika bisnis kita, kita harus setidaknya untuk beberapa waktu mendukung 2 logika penerimaan pesanan, memisahkannya dengan memeriksa seperti if (order.Date > edgeDate) . Dalam hal ini, pengujian kami memiliki peluang untuk jatuh ketika tanggal sistem melewati batas. Ya, kami akan segera memperbaiki ini, dan bahkan membuat dua dari satu tes: satu akan memeriksa logika sebelum tanggal batas, dan yang lainnya setelah. Namun demikian, lebih baik untuk menghindari situasi seperti itu dan segera membuat semua data input konstan.

"Dan di mana DSL?" - kamu bertanya. Faktanya adalah bahwa itu nyaman untuk memasukkan tanggal dalam tes melalui metode Extension, misalnya 3.May(2019) . Bentuk rekaman ini akan dimengerti tidak hanya untuk pengembang, tetapi juga untuk bisnis. Untuk melakukan ini, cukup buat kelas statis seperti itu

 public static class DateConstructionExtensions { public static DateTime May(this int day, int year) => new DateTime(year, 5, day); } 

Secara alami, kurma bukan satu-satunya hal yang menggunakan pola ini. Misalnya, jika kami memperkenalkan jumlah bahan dalam komposisi produk, kami dapat menulis sekitar 42.Grams("flour") .
Objek dan tanggal kuantitatif dibuat dengan mudah melalui metode ekstensi yang sudah dikenal.

Fokus


Mengapa penting untuk membuat tes tetap fokus? Faktanya adalah bahwa tes terfokus lebih mudah dipertahankan, tetapi mereka masih harus didukung. Misalnya, mereka harus diubah ketika mengubah kode dan dihapus ketika melihat fitur lama. Jika tes tidak fokus, maka ketika mengubah logika, Anda perlu memahami tes besar, dan memotong bagian dari fungsionalitas yang diuji dari mereka. Jika tes terfokus dan namanya jelas, maka Anda hanya perlu menghapus tes yang sudah usang dan menulis yang baru. Jika tes memiliki DSL yang baik, maka ini bukan masalah sama sekali.

Jadi, setelah kami selesai menulis DSL, kami berkesempatan untuk membuat tes ini fokus dengan membaginya menjadi 2 tes:

 [Fact] public void WhenAcceptOrder_AddIsCalled() { var order = Create.Order .Dated(3.May(2019)) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyAddWasCalledWith(order); } [Fact] public void WhenAcceptOrder_ReserveIngredientsIsCalled() { var order = Create.Order .Dated(3.May(2019)) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyReserveIngredientsWasCalledWith(order); } 

Kedua tes itu ternyata pendek, jelas, dapat direproduksi, dan fokus.

Harap dicatat bahwa sekarang nama-nama tes mencerminkan tujuan penulisan mereka dan sekarang setiap pengembang yang datang ke proyek saya akan mengerti mengapa masing-masing tes ditulis dan apa yang terjadi dalam tes ini.
Fokus tes membuatnya didukung. Tes yang baik harus fokus.
Dan sekarang, saya sudah bisa mendengar Anda berteriak kepada saya, “Yura, apa-apaan kamu? Kami menulis sejuta kode hanya untuk membuat beberapa tes cantik? " Ya persis. Meskipun kami hanya memiliki beberapa tes, masuk akal untuk berinvestasi dalam DSL dan membuat tes ini dapat dimengerti. Setelah Anda menulis DSL, Anda mendapatkan banyak barang:

  • Menjadi mudah untuk menulis tes baru. Tidak perlu mengatur diri Anda selama 2 jam untuk pengujian unit, cukup ambil dan tulis.
  • Tes menjadi dapat dimengerti dan dibaca. Setiap pengembang yang melihat tes memahami mengapa itu ditulis dan apa yang diperiksa.
  • Ambang untuk mengikuti tes (dan mungkin dalam domain) untuk pengembang baru berkurang. Misalnya, melalui ObjectMother, Anda dapat dengan mudah mengetahui objek apa yang dapat dibuat di domain.
  • Dan akhirnya, itu bagus untuk bekerja dengan tes, dan sebagai hasilnya, kode menjadi lebih didukung.

Kode sumber dan tes sampel tersedia di sini .

Source: https://habr.com/ru/post/id451598/


All Articles