Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Aturan Penerapan TDD di Proyek Lama

Artikel “Sliding Responsibility of the Repository Pattern” mengangkat beberapa pertanyaan, yang sangat sulit untuk dijawab. Apakah kita memerlukan repositori jika mengabaikan detail teknis sepenuhnya tidak mungkin? Seberapa kompleks repositori itu sehingga penambahannya dapat dianggap bermanfaat? Jawaban atas pertanyaan-pertanyaan ini bervariasi tergantung pada penekanan yang ditempatkan dalam pengembangan sistem. Mungkin pertanyaan yang paling sulit adalah sebagai berikut:apakah Anda membutuhkan repositori? Masalah "abstraksi yang mengalir" dan semakin kompleksnya pengkodean dengan peningkatan tingkat abstraksi tidak memungkinkan untuk menemukan solusi yang akan memuaskan kedua sisi pagar. Misalnya, dalam pelaporan, desain niat mengarah pada pembuatan sejumlah besar metode untuk setiap filter dan penyortiran, dan solusi umum menciptakan overhead pengkodean yang besar.

Untuk mendapatkan gambaran lengkap, saya melihat masalah abstraksi dalam hal penerapannya dalam kode warisan. Repositori, dalam hal ini, menarik bagi kami hanya sebagai alat untuk mendapatkan kode yang berkualitas dan tanpa bug. Tentu saja, pola ini bukan satu-satunya hal yang diperlukan untuk penerapan praktik TDD. Setelah makan banyak garam selama pengembangan beberapa proyek besar dan mengamati apa yang berhasil dan apa yang tidak, saya mengembangkan beberapa aturan untuk diri saya sendiri yang membantu saya mengikuti praktik TDD. Saya terbuka untuk kritik yang membangun dan metode penerapan TDD lainnya.

Kata Pengantar

Beberapa mungkin memperhatikan bahwa tidak mungkin menerapkan TDD di proyek lama. Ada pendapat bahwa berbagai jenis tes integrasi (tes UI, ujung ke ujung) lebih cocok untuk mereka karena terlalu sulit untuk memahami kode lama. Juga, Anda dapat mendengar bahwa menulis tes sebelum pengkodean yang sebenarnya hanya menyebabkan hilangnya waktu, karena kita mungkin tidak tahu bagaimana kode akan bekerja. Saya harus mengerjakan beberapa proyek, di mana saya hanya terbatas pada tes integrasi, percaya bahwa tes unit tidak indikatif. Pada saat yang sama, banyak tes ditulis, mereka menjalankan banyak layanan, dll. Akibatnya, hanya satu orang yang dapat memahaminya, yang sebenarnya menulisnya.

Selama latihan saya, saya berhasil mengerjakan beberapa proyek yang sangat besar, di mana ada banyak kode warisan. Beberapa dari mereka menampilkan tes, dan yang lainnya tidak (hanya ada niat untuk menerapkannya). Saya berpartisipasi dalam dua proyek besar, di mana saya mencoba menerapkan pendekatan TDD. Pada tahap awal, TDD dianggap sebagai pengembangan Test First. Akhirnya, perbedaan antara pemahaman yang disederhanakan ini dan persepsi saat ini, yang disingkat BDD, menjadi lebih jelas. Bahasa apa pun yang digunakan, poin utamanya, saya menyebutnya aturan, tetap sama. Seseorang dapat menemukan kesejajaran antara aturan dan prinsip lain dalam menulis kode yang baik.

Aturan 1:Menggunakan Bottom-Up (Inside-Out)

Aturan ini lebih mengacu pada metode analisis dan desain perangkat lunak saat menyematkan potongan kode baru ke dalam proyek kerja.

Ketika Anda sedang merancang sebuah proyek baru, sangatlah wajar untuk membayangkan sebuah sistem secara keseluruhan. Pada tahap ini, Anda mengontrol kumpulan komponen dan fleksibilitas arsitektur di masa mendatang. Oleh karena itu, Anda dapat menulis modul yang dapat dengan mudah dan intuitif terintegrasi satu sama lain. Pendekatan Top-Down seperti itu memungkinkan Anda untuk melakukan desain awal yang baik dari arsitektur masa depan, menjelaskan garis panduan yang diperlukan dan memiliki gambaran lengkap tentang apa yang pada akhirnya Anda inginkan. Setelah beberapa saat, proyek berubah menjadi apa yang disebut kode warisan. Dan kemudian kesenangan dimulai.

Pada tahap ketika perlu untuk menanamkan fungsionalitas baru ke dalam proyek yang sudah ada dengan banyak modul dan dependensi di antara mereka, akan sangat sulit untuk menempatkan semuanya di kepala Anda untuk membuat desain yang tepat. Sisi lain dari masalah ini adalah jumlah pekerjaan yang diperlukan untuk menyelesaikan tugas ini. Oleh karena itu, pendekatan bottom-up akan lebih efektif dalam kasus ini. Dengan kata lain, pertama Anda membuat modul lengkap yang menyelesaikan tugas yang diperlukan, dan kemudian Anda membangunnya ke dalam sistem yang ada, hanya membuat perubahan yang diperlukan. Dalam hal ini, Anda dapat menjamin kualitas modul ini, karena merupakan unit fungsional yang lengkap.

Perlu dicatat bahwa tidak sesederhana itu dengan pendekatan. Misalnya, saat merancang fungsionalitas baru di sistem lama, Anda suka atau tidak suka menggunakan kedua pendekatan tersebut. Selama analisis awal, Anda masih perlu mengevaluasi sistem, kemudian menurunkannya ke level modul, mengimplementasikannya, dan kembali ke level keseluruhan sistem. Menurut pendapat saya, hal utama di sini adalah jangan lupa bahwa modul baru harus memiliki fungsionalitas yang lengkap dan independen, sebagai alat yang terpisah. Semakin ketat Anda mematuhi pendekatan ini, semakin sedikit perubahan yang akan dilakukan pada kode lama.

Aturan 2:Uji hanya kode yang dimodifikasi

Saat bekerja dengan proyek lama, sama sekali tidak perlu menulis tes untuk semua kemungkinan skenario metode/kelas. Selain itu, Anda mungkin tidak menyadari beberapa skenario sama sekali, karena mungkin ada banyak skenario. Proyek sudah di produksi, pelanggan puas, jadi bisa santai. Secara umum, hanya perubahan Anda yang menyebabkan masalah dalam sistem ini. Oleh karena itu, hanya mereka yang harus diuji.

Contoh

Ada modul toko online, yang membuat keranjang barang yang dipilih dan menyimpannya di database. Kami tidak peduli tentang implementasi spesifik. Selesai seperti yang dilakukan – ini adalah kode lama. Sekarang kita perlu memperkenalkan perilaku baru di sini:kirim pemberitahuan ke departemen akuntansi jika biaya kereta melebihi $1000. Berikut adalah kode yang kita lihat. Bagaimana cara memperkenalkan perubahan?

public class EuropeShop : Shop
{
    public override void CreateSale()
    {
        var items = LoadSelectedItemsFromDb();
        var taxes = new EuropeTaxes();
        var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
        var cart = new Cart();
        cart.Add(saleItems);
        taxes.ApplyTaxes(cart);
        SaveToDb(cart);
    }
}

Menurut aturan pertama, perubahan harus minimal dan atomik. Kami tidak tertarik dengan pemuatan data, kami tidak peduli dengan perhitungan pajak dan penyimpanan ke database. Tapi kami tertarik dengan keranjang yang dihitung. Jika ada modul yang melakukan apa yang diperlukan, maka itu akan melakukan tugas yang diperlukan. Itu sebabnya kami melakukan ini.

public class EuropeShop : Shop
{
    public override void CreateSale()
    {
        var items = LoadSelectedItemsFromDb();
        var taxes = new EuropeTaxes();
        var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();
        var cart = new Cart();
        cart.Add(saleItems);
        taxes.ApplyTaxes(cart);

        // NEW FEATURE
        new EuropeShopNotifier().Send(cart);

        SaveToDb(cart);
    }
}

Pemberitahu seperti itu beroperasi sendiri, dapat diuji, dan perubahan yang dilakukan pada kode lama minimal. Ini persis seperti yang dikatakan aturan kedua.

Aturan 3:Kami hanya menguji persyaratan

Untuk membebaskan diri Anda dari sejumlah skenario yang memerlukan pengujian dengan pengujian unit, pikirkan apa yang sebenarnya Anda butuhkan dari sebuah modul. Tulis terlebih dahulu untuk kumpulan kondisi minimum yang dapat Anda bayangkan sebagai persyaratan untuk modul. Himpunan minimum adalah himpunan yang bila ditambah dengan yang baru perilaku modul tidak banyak berubah, dan ketika dilepas modul tidak berfungsi. Pendekatan BDD banyak membantu dalam kasus ini.

Juga, bayangkan bagaimana kelas lain yang merupakan klien dari modul Anda akan berinteraksi dengannya. Apakah Anda perlu menulis 10 baris kode untuk mengkonfigurasi modul Anda? Semakin sederhana komunikasi antara bagian-bagian sistem, semakin baik. Oleh karena itu, lebih baik memilih modul yang bertanggung jawab untuk sesuatu yang spesifik dari kode lama. SOLID akan membantu dalam kasus ini.

Contoh

Sekarang mari kita lihat bagaimana semua yang dijelaskan di atas akan membantu kita dengan kode. Pertama, pilih semua modul yang hanya terkait secara tidak langsung dengan pembuatan troli. Beginilah cara tanggung jawab untuk modul didistribusikan.

public class EuropeShop : Shop
{
    public override void CreateSale()
    {
        // 1) load from DB
        var items = LoadSelectedItemsFromDb();

        // 2) Tax-object creates SaleItem and
        // 4) goes through items and apply taxes
        var taxes = new EuropeTaxes();
        var saleItems = items.Select(item => taxes.ApplyTaxes(item)).ToList();

        // 3) creates a cart and 4) applies taxes
        var cart = new Cart();
        cart.Add(saleItems);
        taxes.ApplyTaxes(cart);

        new EuropeShopNotifier().Send(cart);

        // 4) store to DB
        SaveToDb(cart);
    }
}

Dengan cara ini mereka dapat dibedakan. Tentu saja, perubahan seperti itu tidak dapat dilakukan sekaligus dalam sistem yang besar, tetapi dapat dilakukan secara bertahap. Misalnya, ketika perubahan terkait dengan modul pajak, Anda dapat menyederhanakan bagaimana bagian lain dari sistem bergantung padanya. Ini dapat membantu menghilangkan ketergantungan tinggi dan menggunakannya di masa mendatang sebagai alat mandiri.

public class EuropeShop : Shop
{
    public override void CreateSale()
    {
        // 1) extracted to a repository
        var itemsRepository = new ItemsRepository();
        var items = itemsRepository.LoadSelectedItems();
			
        // 2) extracted to a mapper
        var saleItems = items.ConvertToSaleItems();
			
        // 3) still creates a cart
        var cart = new Cart();
        cart.Add(saleItems);
			
        // 4) all routines to apply taxes are extracted to the Tax-object
        new EuropeTaxes().ApplyTaxes(cart);
			
        new EuropeShopNotifier().Send(cart);
			
        // 5) extracted to a repository
        itemsRepository.Save(cart);
    }
}

Adapun tes, skenario ini akan cukup. Sejauh ini, implementasinya tidak menarik bagi kami.

public class EuropeTaxesTests
{
    public void Should_not_fail_for_null() { }

    public void Should_apply_taxes_to_items() { }

    public void Should_apply_taxes_to_whole_cart() { }

    public void Should_apply_taxes_to_whole_cart_and_change_items() { }
}

public class EuropeShopNotifierTests
{
    public void Should_not_send_when_less_or_equals_to_1000() { }

    public void Should_send_when_greater_than_1000() { }

    public void Should_raise_exception_when_cannot_send() { }
}

Aturan 4:Tambahkan hanya kode yang diuji

Seperti yang saya tulis sebelumnya, Anda harus meminimalkan perubahan pada kode lama. Untuk melakukan ini, kode lama dan baru/dimodifikasi dapat dipisahkan. Kode baru dapat ditempatkan dalam metode yang dapat diperiksa menggunakan unit test. Pendekatan ini akan membantu mengurangi risiko terkait. Ada dua teknik yang telah dijelaskan dalam buku “Bekerja Secara Efektif dengan Kode Warisan” (tautan ke buku di bawah).

Metode/kelas Sprout – teknik ini memungkinkan Anda untuk menyematkan kode baru yang sangat aman ke dalam kode lama. Cara saya menambahkan pemberi tahu adalah contoh dari pendekatan ini.

Metode bungkus – sedikit lebih rumit, tetapi intinya sama. Itu tidak selalu berfungsi, tetapi hanya dalam kasus di mana kode baru dipanggil sebelum/sesudah yang lama. Saat menetapkan tanggung jawab, dua panggilan metode ApplyTaxes digantikan oleh satu panggilan. Untuk ini, perlu mengubah metode kedua agar logikanya tidak terlalu putus dan bisa diperiksa. Seperti itulah tampilan kelas sebelum perubahan.

public class EuropeTaxes : Taxes
{
    internal override SaleItem ApplyTaxes(Item item)
    {
        var saleItem = new SaleItem(item)
        {
            SalePrice = item.Price*1.2m
        };
        return saleItem;
    }

    internal override void ApplyTaxes(Cart cart)
    {
        if (cart.TotalSalePrice <= 300m) return;
        var exclusion = 30m/cart.SaleItems.Count;
        foreach (var item in cart.SaleItems)
            if (item.SalePrice - exclusion > 100m)
                item.SalePrice -= exclusion;
    }
}

Dan di sini bagaimana tampilannya. Logika bekerja dengan elemen gerobak sedikit berubah, tetapi secara umum, semuanya tetap sama. Dalam hal ini, metode lama memanggil terlebih dahulu ApplyToItems baru, dan kemudian versi sebelumnya. Inilah inti dari teknik ini.

public class EuropeTaxes : Taxes
{
    internal override void ApplyTaxes(Cart cart)
    {
        ApplyToItems(cart);
        ApplyToCart(cart);
    }

    private void ApplyToItems(Cart cart)
    {
        foreach (var item in cart.SaleItems)
            item.SalePrice = item.Price*1.2m;
    }

    private void ApplyToCart(Cart cart)
    {
        if (cart.TotalSalePrice <= 300m) return;
        var exclusion = 30m / cart.SaleItems.Count;
        foreach (var item in cart.SaleItems)
            if (item.SalePrice - exclusion > 100m)
                item.SalePrice -= exclusion;
    }
}

Aturan 5:“Hancurkan” dependensi tersembunyi

Ini adalah aturan tentang kejahatan terbesar dalam kode lama:penggunaan kode baru operator di dalam metode satu objek untuk membuat objek lain, repositori, atau objek kompleks lainnya. Mengapa itu buruk? Penjelasan paling sederhana adalah bahwa ini membuat bagian-bagian dari sistem sangat terhubung dan membantu mengurangi koherensinya. Bahkan lebih pendek:mengarah pada pelanggaran prinsip "kopel rendah, kohesi tinggi". Jika Anda melihat sisi lain, maka kode ini terlalu sulit untuk diekstraksi menjadi alat independen yang terpisah. Menyingkirkan dependensi tersembunyi seperti itu sekaligus sangat melelahkan. Tapi ini bisa dilakukan secara bertahap.

Pertama, Anda harus mentransfer inisialisasi semua dependensi ke konstruktor. Secara khusus, ini berlaku untuk baru operator dan pembuatan kelas. Jika Anda memiliki ServiceLocator untuk mendapatkan instance kelas, Anda juga harus menghapusnya ke konstruktor, tempat Anda dapat menarik semua antarmuka yang diperlukan darinya.

Kedua, variabel yang menyimpan instance dari objek/repositori eksternal harus memiliki tipe abstrak, dan antarmuka yang lebih baik. Antarmuka lebih baik karena memberikan lebih banyak kemampuan kepada pengembang. Akibatnya, ini akan memungkinkan pembuatan alat atomik dari modul.

Ketiga, jangan tinggalkan lembar metode yang besar. Ini dengan jelas menunjukkan bahwa metode ini melakukan lebih dari yang ditentukan dalam namanya. Ini juga merupakan indikasi kemungkinan pelanggaran SOLID, Hukum Demeter.

Contoh

Sekarang mari kita lihat bagaimana kode yang membuat cart telah diubah. Hanya blok kode yang membuat keranjang tetap tidak berubah. Sisanya ditempatkan ke kelas eksternal dan dapat diganti dengan implementasi apa pun. Sekarang kelas EuropeShop mengambil bentuk alat atom yang membutuhkan hal-hal tertentu yang secara eksplisit diwakili dalam konstruktor. Kode menjadi lebih mudah dipahami.

public class EuropeShop : Shop
{
    private readonly IItemsRepository _itemsRepository;
    private readonly Taxes.Taxes _europeTaxes;
    private readonly INotifier _europeShopNotifier;

    public EuropeShop()
    {
        _itemsRepository = new ItemsRepository();
        _europeTaxes = new EuropeTaxes();
        _europeShopNotifier = new EuropeShopNotifier();
    }

    public override void CreateSale()
    {
        var items = _itemsRepository.LoadSelectedItems();
        var saleItems = items.ConvertToSaleItems();

        var cart = new Cart();
        cart.Add(saleItems);

        _europeTaxes.ApplyTaxes(cart);
        _europeShopNotifier.Send(cart);
        _itemsRepository.Save(cart);
    }
}SCRIPT

Aturan 6:Semakin sedikit tes besar, semakin baik

Tes besar adalah tes integrasi berbeda yang mencoba menguji skrip pengguna. Tidak diragukan lagi, mereka penting, tetapi untuk memeriksa logika beberapa JIKA di kedalaman kode sangat mahal. Menulis tes ini membutuhkan waktu yang sama, jika tidak lebih, seperti menulis fungsionalitas itu sendiri. Mendukung mereka seperti kode warisan lainnya, yang sulit diubah. Tapi ini hanya ujian!

Penting untuk memahami tes mana yang diperlukan dan dengan jelas mematuhi pemahaman ini. Jika Anda memerlukan pemeriksaan integrasi, tulis serangkaian pengujian minimum, termasuk skenario interaksi positif dan negatif. Jika Anda perlu menguji algoritme, tulis satu set pengujian unit minimal.

Aturan 7:Jangan menguji metode pribadi

Metode pribadi bisa terlalu rumit atau berisi kode yang tidak dipanggil dari metode publik. Saya yakin bahwa alasan lain apa pun yang dapat Anda pikirkan akan terbukti menjadi karakteristik kode atau desain yang "buruk". Kemungkinan besar, bagian dari kode dari metode pribadi harus dibuat menjadi metode/kelas yang terpisah. Periksa apakah prinsip pertama SOLID dilanggar. Ini adalah alasan pertama mengapa hal itu tidak layak dilakukan. Yang kedua adalah bahwa dengan cara ini Anda tidak memeriksa perilaku seluruh modul, tetapi bagaimana modul mengimplementasikannya. Implementasi internal dapat berubah terlepas dari perilaku modul. Oleh karena itu, dalam kasus ini, Anda mendapatkan tes yang rapuh, dan dibutuhkan lebih banyak waktu daripada yang diperlukan untuk mendukungnya.

Untuk menghindari kebutuhan untuk menguji metode pribadi, tunjukkan kelas Anda sebagai seperangkat alat atom dan Anda tidak tahu bagaimana penerapannya. Anda mengharapkan beberapa perilaku yang Anda uji. Sikap ini juga berlaku untuk kelas-kelas dalam konteks majelis. Kelas yang tersedia untuk klien (dari majelis lain) akan bersifat publik, dan kelas yang melakukan pekerjaan internal – pribadi. Meskipun, ada perbedaan dari metode. Kelas internal dapat menjadi kompleks, sehingga dapat diubah menjadi kelas internal dan juga diuji.

Contoh

Misalnya, untuk menguji satu kondisi dalam metode pribadi kelas EuropeTaxes, saya tidak akan menulis tes untuk metode ini. Saya berharap bahwa pajak akan diterapkan dengan cara tertentu, jadi tes akan mencerminkan perilaku ini. Dalam pengujian, saya menghitung secara manual apa yang seharusnya menjadi hasilnya, menganggapnya sebagai standar, dan mengharapkan hasil yang sama dari kelas.

public class EuropeTaxes : Taxes
{
    // code skipped

    private void ApplyToCart(Cart cart)
    {
        if (cart.TotalSalePrice <= 300m) return; // <<< I WANT TO TEST THIS CONDIFTION
        var exclusion = 30m / cart.SaleItems.Count;
        foreach (var item in cart.SaleItems)
            if (item.SalePrice - exclusion > 100m)
                item.SalePrice -= exclusion;
    }
}

// test suite
public class EuropeTaxesTests
{
    // code skipped

    [Fact]
    public void Should_apply_taxes_to_cart_greater_300()
    {
        #region arrange
        // list of items which will create a cart greater 300
        var saleItems = new List<Item>(new[]{new Item {Price = 83.34m},
            new Item {Price = 83.34m},new Item {Price = 83.34m}})
            .ConvertToSaleItems();
        var cart = new Cart();
        cart.Add(saleItems);

        const decimal expected = 83.34m*3*1.2m;
        #endregion

        // act
        new EuropeTaxes().ApplyTaxes(cart);

        // assert
        Assert.Equal(expected, cart.TotalSalePrice);
    }
}

Aturan 8:Jangan menguji algoritme metode

Beberapa orang memeriksa jumlah panggilan metode tertentu, memverifikasi panggilan itu sendiri, dll., Dengan kata lain, memeriksa pekerjaan internal metode. Ini sama buruknya dengan pengujian yang pribadi. Perbedaannya hanya pada lapisan aplikasi cek tersebut. Pendekatan ini sekali lagi memberikan banyak tes yang rapuh, sehingga beberapa orang tidak menggunakan TDD dengan benar.

Baca selengkapnya…

Aturan 9:Jangan ubah kode lama tanpa pengujian

Ini adalah aturan yang paling penting karena mencerminkan keinginan tim untuk mengikuti jalan ini. Tanpa keinginan untuk bergerak ke arah ini, semua yang telah dikatakan di atas tidak memiliki arti khusus. Karena jika pengembang tidak mau menggunakan TDD (tidak mengerti artinya, tidak melihat manfaatnya, dll), maka manfaat sebenarnya akan kabur dengan diskusi terus-menerus betapa sulit dan tidak efisiennya itu.

Jika Anda akan menggunakan TDD, diskusikan ini dengan tim Anda, tambahkan ke Definisi Selesai, dan terapkan. Pada awalnya, itu akan sulit, seperti dengan segala sesuatu yang baru. Seperti seni apa pun, TDD membutuhkan latihan terus-menerus, dan kesenangan datang saat Anda belajar. Secara bertahap, akan ada lebih banyak tes unit tertulis, Anda akan mulai merasakan "kesehatan" sistem Anda dan mulai menghargai kesederhanaan penulisan kode, menjelaskan persyaratan pada tahap pertama. Ada studi TDD yang dilakukan pada proyek besar nyata di Microsoft dan IBM, yang menunjukkan pengurangan bug dalam sistem produksi dari 40% menjadi 80% (lihat tautan di bawah).

Bacaan Lebih Lanjut

  1. Buku “Bekerja Secara Efektif dengan Kode Warisan” oleh Michael Feathers
  2. TDD saat mencapai leher Anda dalam Kode Legacy
  3. Mematahkan Ketergantungan Tersembunyi
  4. Siklus Hidup Kode Lama
  5. Haruskah Anda menguji unit metode pribadi di kelas?
  6. Internal pengujian unit
  7. 5 Kesalahpahaman Umum Tentang TDD &Pengujian Unit
  8. Hukum Demeter

  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Apakah Anda Membuat Kesalahan Ini Saat Menggunakan SQL CURSOR?

  2. Software Database Terbaik untuk Developer (Edisi 2022)

  3. Cara Menginstal dan Mengonfigurasi Zabbix di Ubuntu 20.04

  4. Prosedur Tersimpan Khusus untuk Mendapatkan Status Cadangan Basis Data Terbaru

  5. Hekaton dengan twist:TVP dalam memori – Bagian 1