Praktik terbaik untuk metode pengujian unit yang banyak menggunakan cache?

17

Saya memiliki sejumlah metode logika bisnis yang menyimpan dan mengambil (dengan memfilter) objek dan daftar objek dari cache.

Mempertimbangkan

IList<TObject> AllFromCache() { ... }

TObject FetchById(guid id) { ... }

IList<TObject> FilterByPropertry(int property) { ... }

Fetch..dan Filter..akan memanggil AllFromCachemana yang akan mengisi cache dan kembali jika tidak ada dan kembali saja jika ada.

Saya biasanya menghindar dari unit yang menguji ini. Apa praktik terbaik untuk Pengujian Unit terhadap jenis struktur ini?

Saya menganggap mengisi cache pada TestInitialize dan menghapus pada TestCleanup tapi itu tidak terasa benar bagi saya, (mungkin itu bisa).

NikolaiDante
sumber

Jawaban:

18

Jika Anda ingin Tes Unit benar, maka Anda harus mengejek cache: menulis objek tiruan yang mengimplementasikan antarmuka yang sama dengan cache, tetapi alih-alih menjadi cache, ia tetap melacak panggilan yang diterimanya, dan selalu mengembalikan apa yang sebenarnya cache harus dikembalikan sesuai dengan test case.

Tentu saja cache itu sendiri juga perlu pengujian unit, untuk itu Anda harus mengejek apa pun itu tergantung, dan sebagainya.

Apa yang Anda gambarkan, menggunakan objek cache sebenarnya tetapi menginisialisasi ke kondisi yang diketahui dan membersihkan setelah tes, lebih seperti tes integrasi, karena Anda menguji beberapa unit dalam konser.

tammmer
sumber
+1 ini adalah pendekatan terbaik. Tes unit untuk memeriksa logika kemudian uji integrasi untuk benar-benar memverifikasi cache berfungsi seperti yang Anda harapkan.
Tom Squires
10

The Tunggal Tanggung Jawab Prinsip adalah teman terbaik Anda di sini.

Pertama-tama, pindahkan AllFromCache () ke kelas repositori dan beri nama GetAll (). Itu diambil dari cache adalah detail implementasi dari repositori dan tidak boleh diketahui oleh kode panggilan.

Ini membuat pengujian kelas penyaringan Anda menyenangkan dan mudah. Tidak lagi peduli dari mana Anda mendapatkannya.

Kedua, bungkus kelas yang mendapatkan data dari database (atau di mana pun) dalam pembungkus caching.

AOP adalah teknik yang bagus untuk ini. Ini salah satu dari beberapa hal yang sangat bagus.

Menggunakan alat-alat seperti PostSharp , Anda dapat mengaturnya sehingga metode apa pun yang ditandai dengan atribut yang dipilih akan di-cache. Namun, jika ini adalah satu-satunya hal yang Anda caching, Anda tidak perlu pergi sejauh memiliki kerangka kerja AOP. Hanya memiliki Repositori dan Pembungkus Caching yang menggunakan antarmuka yang sama dan menyuntikkan itu ke kelas panggilan.

misalnya.

public class ProductManager
{
    private IProductRepository ProductRepository { get; set; }

    public ProductManager
    {
        ProductRepository = productRepository;
    }

    Product FetchById(guid id) { ... }

    IList<Product> FilterByPropertry(int property) { ... }
}

public interface IProductRepository
{
    IList<Product> GetAll();
}

public class SqlProductRepository : IProductRepository
{
    public IList<Product> GetAll()
    {
        // DB Connection, fetch
    }
}

public class CachedProductRepository : IProductRepository
{
    private IProductRepository ProductRepository { get; set; }

    public CachedProductRepository (IProductRepository productRepository)
    {
        ProductRepository = productRepository;
    }

    public IList<Product> GetAll()
    {
        // Check cache, if exists then return, 
        // if not then call GetAll() on inner repository
    }
}

Lihat bagaimana Anda telah menghapus pengetahuan implementasi repositori dari ProductManager? Lihat juga bagaimana Anda mematuhi Prinsip Tanggung Jawab Tunggal dengan memiliki kelas yang menangani ekstraksi data, kelas yang menangani pengambilan data, dan kelas yang menangani caching?

Anda sekarang dapat instantiate ProductManager dengan salah satu dari Repositori tersebut dan mendapatkan caching ... atau tidak. Ini sangat berguna nantinya ketika Anda mendapatkan bug yang membingungkan yang Anda duga adalah hasil dari cache.

productManager = new ProductManager(
                         new SqlProductRepository()
                         );

productManager = new ProductManager(
                         new CachedProductRepository(new SqlProductRepository())
                         );

(Jika Anda menggunakan wadah IOC, bahkan lebih baik. Seharusnya jelas cara beradaptasi.)

Dan, dalam tes ProductManager Anda

IProductRepository repo = MockRepository.GenerateStrictMock<IProductRepository>();

Tidak perlu menguji cache sama sekali.

Sekarang pertanyaannya menjadi: Haruskah saya menguji CachedProductRepository itu? Saya sarankan tidak. Cache ini cukup tidak pasti. Kerangka kerja melakukan hal-hal dengan itu yang di luar kendali Anda. Seperti, hanya menghapus barang-barang dari itu ketika sudah terlalu penuh, misalnya. Anda akan berakhir dengan tes yang gagal sekali dalam bulan biru dan Anda tidak akan pernah mengerti mengapa.

Dan, setelah melakukan perubahan yang saya sarankan di atas, benar-benar tidak banyak logika untuk diuji di sana. Tes yang sangat penting, metode penyaringan, akan ada di sana dan sepenuhnya disarikan dari detail GetAll (). GetAll () baru saja ... mendapatkan semuanya. Dari suatu tempat.

pdr
sumber
Apa yang Anda lakukan jika Anda menggunakan CachedProductRepository di ProductManager tetapi ingin menggunakan metode yang ada di SQLProductRepository?
Jonathan
@ Jonathan: "Hanya memiliki Repositori dan Pembungkus Caching yang menggunakan antarmuka yang sama" - jika mereka memiliki antarmuka yang sama, Anda dapat menggunakan metode yang sama. Kode panggilan tidak perlu tahu apa-apa tentang implementasi.
pdr
3

Pendekatan yang Anda sarankan adalah apa yang akan saya lakukan. Dengan uraian Anda, hasil metode harus sama apakah objek ada di cache atau tidak: Anda harus tetap mendapatkan hasil yang sama. Itu mudah untuk diuji dengan mengatur cache dengan cara tertentu sebelum setiap tes. Mungkin ada beberapa kasus tambahan seperti jika penunjuk adalah nullatau tidak ada objek memiliki properti yang diminta; itu juga bisa diuji.

Selain itu, Anda mungkin menganggapnya diharapkan bahwa objek tersebut ada dalam cache setelah metode Anda kembali, terlepas dari apakah itu ada dalam cache di tempat pertama. Ini kontroversial, karena beberapa orang (termasuk saya sendiri) berpendapat bahwa Anda peduli dengan apa yang Anda dapatkan kembali dari antarmuka Anda, bukan bagaimana Anda mendapatkannya (yaitu pengujian Anda bahwa antarmuka berfungsi seperti yang diharapkan, bukan karena ia memiliki implementasi khusus). Jika Anda menganggapnya penting, Anda memiliki kesempatan untuk mengujinya.


sumber
1

Saya mempertimbangkan mengisi cache pada TestInitialize dan menghapus pada TestCleanup tetapi itu tidak terasa benar bagi saya

Sebenarnya, itulah satu-satunya cara yang benar untuk dilakukan. Itulah dua fungsi yang ada di sana: untuk mengatur prasyarat, dan membersihkan. Jika prasyarat tidak terpenuhi, program Anda mungkin tidak berfungsi.

BЈовић
sumber
0

Saya sedang mengerjakan beberapa tes yang menggunakan cache baru-baru ini. Saya membuat pembungkus di sekitar kelas yang bekerja dengan cache, dan kemudian memiliki pernyataan bahwa pembungkus ini dipanggil.

Saya melakukan ini terutama karena kelas yang ada yang bekerja dengan cache statis.

Daniel Hollinrake
sumber
0

Sepertinya Anda ingin menguji logika caching, tetapi bukan populating logic. Jadi saya menyarankan agar Anda mengejek apa yang tidak perlu Anda uji - mengisi.

AllFromCache()Metode Anda menangani pengisian cache, dan itu harus didelegasikan ke sesuatu yang lain, seperti pemasok nilai. Jadi kode Anda akan terlihat seperti

private Supplier<TObject> supplier;

IList<TObject> AllFromCache() {
    if (!cacheInitialized) {
        //whatever logic needed to fill the cache
        cache.putAll(supplier.getValues());
        cacheInitialized = true;
    }

    return  cache.getAll();
}

Sekarang Anda dapat mengejek pemasok untuk pengujian, untuk mengembalikan beberapa nilai yang telah ditentukan. Dengan begitu, Anda dapat menguji pemfilteran dan pengambilan yang sebenarnya, dan tidak memuat objek.

jmruc
sumber