Unit Testing dengan tabel pencarian masif?

8

Sistem kami disusun sedemikian rupa sehingga kami mendapatkan banyak informasi penting untuk perhitungan kami dan logika lainnya dari tabel jenis pencarian. Contohnya adalah semua jenis tingkat yang berbeda (seperti suku bunga atau tingkat kontribusi), tanggal (seperti tanggal efektif), dan semua jenis info misc yang berbeda.

Mengapa mereka memutuskan untuk menyusun semuanya seperti ini? Karena beberapa info ini sering berubah. Sebagai contoh, beberapa tarif kami berubah setiap tahun. Mereka ingin mencoba meminimalkan perubahan kode. Harapannya hanya bahwa tabel pencarian akan berubah dan kode hanya akan berfungsi (tidak ada perubahan kode).

Sayangnya, saya pikir ini akan membuat pengujian unit menantang. Beberapa logika mungkin membuat 100+ pencarian berbeda. Meskipun saya pasti dapat membuat objek yang dapat dipermainkan yang mengembalikan tarif kami, akan ada pengaturan yang cukup besar. Saya pikir itu baik itu atau saya harus menggunakan tes integrasi (dan memukul database itu). Apakah saya benar atau ada cara yang lebih baik? Ada saran?

Sunting:
Maaf atas tanggapan yang tertunda tetapi saya mencoba untuk merendam semuanya sementara pada saat yang sama menyulap banyak hal lainnya. Saya juga ingin mencoba bekerja melalui implementasi dan pada saat yang sama. Saya mencoba berbagai pola untuk mencoba merancang solusi untuk sesuatu yang saya sukai. Saya mencoba pola pengunjung yang tidak saya sukai. Pada akhirnya saya akhirnya menggunakan arsitektur bawang. Apakah saya senang dengan hasilnya? Semacam. Saya pikir itu adalah apa adanya. Tabel pencarian membuatnya jauh lebih menantang.

Berikut ini adalah contoh kecil (saya menggunakan fakeiteasy) dari kode pengaturan untuk tes untuk tingkat yang berubah tahunan:

private void CreateStubsForCrsOS39Int()
{
    CreateMacIntStub(0, 1.00000m);
    CreateMacIntStub(1, 1.03000m);
    CreateMacIntStub(2, 1.06090m);
    CreateMacIntStub(3, 1.09273m);
    CreateMacIntStub(4, 1.12551m);
    CreateMacIntStub(5, 1.15928m);
    CreateMacIntStub(6, 1.19406m);
    CreateMacIntStub(7, 1.22988m);
    CreateMacIntStub(8, 1.26678m);
    CreateMacIntStub(9, 1.30478m);
    CreateMacIntStub(10, 1.34392m);
    CreateMacIntStub(11, 1.38424m);
    CreateMacIntStub(12, 1.42577m);
    CreateMacIntStub(13, 1.46854m);
    CreateMacIntStub(14, 1.51260m);
    CreateMacIntStub(15, 1.55798m);
    CreateMacIntStub(16, 1.60472m);
    CreateMacIntStub(17, 1.65286m);
    CreateMacIntStub(18, 1.70245m);
    CreateMacIntStub(19, 1.75352m);
    CreateMacIntStub(20, 1.80613m);
    CreateMacIntStub(21, 1.86031m);
    CreateMacIntStub(22, 1.91612m);
    CreateMacIntStub(23, 1.97360m);
    CreateMacIntStub(24, 2.03281m);
    CreateMacIntStub(25, 2.09379m);
    CreateMacIntStub(26, 2.15660m);
    CreateMacIntStub(27, 2.24286m);
    CreateMacIntStub(28, 2.28794m);
    CreateMacIntStub(29, 2.35658m);
    CreateMacIntStub(30, 2.42728m);
    CreateMacIntStub(31, 2.50010m);
    CreateMacIntStub(32, 2.57510m);
    CreateMacIntStub(33, 2.67810m);
    CreateMacIntStub(34, 2.78522m);
    CreateMacIntStub(35, 2.89663m);
    CreateMacIntStub(36, 3.01250m);
    CreateMacIntStub(37, 3.13300m);
    CreateMacIntStub(38, 3.25832m);
    CreateMacIntStub(39, 3.42124m);
    CreateMacIntStub(40, 3.59230m);
    CreateMacIntStub(41, 3.77192m);
    CreateMacIntStub(42, 3.96052m);
    CreateMacIntStub(43, 4.19815m);
    CreateMacIntStub(44, 4.45004m);
    CreateMacIntStub(45, 4.71704m);
    CreateMacIntStub(46, 5.00006m);
    CreateMacIntStub(47, 5.30006m);
    CreateMacIntStub(48, 5.61806m);
    CreateMacIntStub(49, 5.95514m);
    CreateMacIntStub(50, 6.31245m);
    CreateMacIntStub(51, 6.69120m);
    CreateMacIntStub(52, 7.09267m);
    CreateMacIntStub(53, 7.51823m);
    CreateMacIntStub(54, 7.96932m);
    CreateMacIntStub(55, 8.44748m);
    CreateMacIntStub(56, 8.95433m);
    CreateMacIntStub(57, 9.49159m);
    CreateMacIntStub(58, 10.06109m);
    CreateMacIntStub(59, 10.66476m);
    CreateMacIntStub(60, 11.30465m);
    CreateMacIntStub(61, 11.98293m);
    CreateMacIntStub(62, 12.70191m);
    CreateMacIntStub(63, 13.46402m);
    CreateMacIntStub(64, 14.27186m);
    CreateMacIntStub(65, 15.12817m);
    CreateMacIntStub(66, 16.03586m);
    CreateMacIntStub(67, 16.99801m);
    CreateMacIntStub(68, 18.01789m);
    CreateMacIntStub(69, 19.09896m);
    CreateMacIntStub(70, 20.24490m);
    CreateMacIntStub(71, 21.45959m);
    CreateMacIntStub(72, 22.74717m);
    CreateMacIntStub(73, 24.11200m);
    CreateMacIntStub(74, 25.55872m);
    CreateMacIntStub(75, 27.09224m);
    CreateMacIntStub(76, 28.71778m);

}

private void CreateMacIntStub(byte numberOfYears, decimal returnValue)
{
    A.CallTo(() => _macRateRepository.GetMacArIntFactor(numberOfYears)).Returns(returnValue);
}

Berikut adalah beberapa kode pengaturan untuk kurs yang dapat berubah kapan saja (mungkin bertahun-tahun sebelum suku bunga baru diperkenalkan):

private void CreateStubForGenMbrRateTable()
{
    _rate = A.Fake<IRate>();
    A.CallTo(() => _rate.GetRateFigure(17, A<System.DateTime>.That.Matches(x => x < new System.DateTime(1971, 7, 1)))).Returns(1.030000000m);

    A.CallTo(() => _rate.GetRateFigure(17, 
        A<System.DateTime>.That.Matches(x => x < new System.DateTime(1977, 7, 1) && x >= new System.DateTime(1971,7,1)))).Returns(1.040000000m);

    A.CallTo(() => _rate.GetRateFigure(17,
        A<System.DateTime>.That.Matches(x => x < new System.DateTime(1981, 7, 1) && x >= new System.DateTime(1971, 7, 1)))).Returns(1.050000000m);
    A.CallTo(
        () => _rate.GetRateFigure(17, A<System.DateTime>.That.IsGreaterThan(new System.DateTime(1981, 6, 30).AddHours(23)))).Returns(1.060000000m);
}

Berikut adalah konstruktor untuk salah satu Objek Domain saya:

public abstract class OsEarnDetail: IOsCalcableDetail
{
    private readonly OsEarnDetailPoco _data;
    private readonly IOsMacRateRepository _macRates;
    private readonly IRate _rate;
    private const int RdRate = (int) TRSEnums.RateTypeConstants.ertRD;

    public OsEarnDetail(IOsMacRateRepository macRates,IRate rate, OsEarnDetailPoco data)
    {
        _macRates = macRates;
        _rate = rate;
        _data = data;
    }

Jadi mengapa saya tidak menyukainya? Tes yang sudah ada akan bekerja tetapi siapa pun yang menambahkan tes baru di masa mendatang harus melihat melalui kode pengaturan ini untuk memastikan setiap tarif baru ditambahkan. Saya mencoba membuatnya sejelas mungkin dengan menggunakan nama tabel sebagai bagian dari nama fungsi tetapi saya rasa itu adalah apa adanya :)

coding4fun
sumber

Jawaban:

16

Anda masih bisa menulis unit test. Yang dijelaskan oleh pertanyaan Anda adalah skenario di mana Anda memiliki beberapa sumber data yang menjadi sandaran kode Anda. Sumber data ini perlu menghasilkan data palsu yang sama di semua tes Anda. Namun, Anda tidak ingin kekacauan yang terkait dengan mengatur respons untuk setiap tes tunggal. Yang Anda butuhkan adalah tes palsu

Tes palsu adalah implementasi dari sesuatu yang terlihat seperti bebek dan dukun seperti bebek, tetapi tidak melakukan apa pun selain memberikan respons yang konsisten untuk keperluan pengujian.


Dalam kasus Anda, Anda mungkin memiliki IExchangeRateLookupantarmuka dan implementasi produksi untuk itu

public interface IExchangeRateLookup
{
    float Find(Currency currency);
}

public class DatabaseExchangeRateLookup : IExchangeRateLookup
{
    public float Find(Currency currency)
    {
        return SomethingFromTheDatabase(currency);
    }
}

Dengan bergantung pada antarmuka dalam kode yang diuji, Anda dapat mengirimkan apa pun yang mengimplementasikannya, termasuk yang palsu

public class ExchangeRateLookupFake : IExchangeRateLookup
{
    private Dictionary<Currency, float> _lookup = new Dictionary<Currency, float>();

    public ExchangeRateLookupFake()
    {
        _lookup = IntialiseLookupWithFakeValues();
    }

    public float Find(Currency currency)
    {
        return _lookup[currency];
    }
}
Andy Hunt
sumber
8

Fakta bahwa:

Beberapa logika mungkin membuat 100+ pencarian berbeda.

tidak relevan dalam konteks pengujian unit. Tes unit berfokus pada sebagian kecil kode, biasanya metode, dan tidak mungkin bahwa metode tunggal membutuhkan 100+ tabel pencarian (jika ya, refactoring harus menjadi perhatian utama Anda; pengujian datang setelah itu). Kecuali maksud Anda 100+ pencarian dalam satu lingkaran ke tabel yang sama, dalam hal ini, Anda baik-baik saja.

Kompleksitas menambahkan bertopik dan mengolok-olok untuk pencarian tersebut seharusnya tidak mengganggu Anda baik pada skala tes unit tunggal. Dalam pengujian, Anda hanya akan mematikan / mencemooh lookup yang sebenarnya digunakan oleh metode ini. Anda tidak hanya tidak akan memiliki banyak dari mereka, tetapi juga bertopik atau mengolok-olok itu akan sangat sederhana. Misalnya, mereka dapat mengembalikan nilai tunggal, apa pun metode yang dicari (seolah-olah pencarian aktual diisi dengan nomor yang sama).

Ketika kompleksitas akan menjadi masalah adalah ketika Anda harus menguji logika bisnis. 100+ pencarian mungkin berarti ribuan dan ribuan kasus bisnis berbeda untuk diuji (bahkan di luar pencarian), yang berarti ribuan dan ribuan unit test.

Ilustrasi

Misalnya, dalam konteks kubus OLAP, Anda mungkin memiliki metode yang bergantung pada dua kubus, satu dengan dua dimensi dan satu dengan lima dimensi:

public class HelloWorld
{
    // Intentionally hardcoded cubes.
    private readonly OlapCube olapVersions = new VersionsOlapCube();
    private readonly OlapCube olapStatistics = new StatisticsOlapCube();

    ...

    public int Demo(...)
    {
        ...
        this.olapVersions.Find(a, b);
        ...
        this.olapStatistics.Find(c, d, 0, e, 0);
        ...
    }
}

Seperti, metode tidak dapat diuji unit. Langkah pertama adalah untuk memungkinkan untuk mengganti kubus OLAP dengan bertopik. Salah satu cara untuk melakukannya adalah melalui Injeksi Ketergantungan.

public class HelloWorld
{
    // Notice the interface instead of a class.
    private readonly IOlapCube olapVersions;
    private readonly IOlapCube olapStatistics;

    // Constructor.
    public HelloWorld(
        IVersionsOlapCube olapVersions, IStatisticsOlapCube olapStatistics)
    {
    }

    ...

    public void Demo(...)
    {
        ...
        this.olapVersions.Find(a, b);
        ...
        this.olapStatistics.Find(c, d, 0, e, 0);
        ...
    }
}

Sekarang tes unit dapat menyuntikkan rintisan seperti ini:

class OlapCubeStub : IOlapCube
{
    public OlapValue Find(params int[] values)
    {
        return OlapValue.FromInt(1); // Constant value here.
    }
}

dan digunakan seperti itu:

var helloWorld = new HelloWorld(new OlapCubeStub(), new OlapCubeStub());
var actual = helloWorld.Demo();
var expected = 9;
this.AssertEquals(expected, actual);
Arseni Mourzenko
sumber
Terima kasih balasannya. Sementara saya pikir refactoring jelas pintar apa yang Anda lakukan dalam kasus di mana Anda memiliki perhitungan sangat kompleks (sebut saja CalcFoo ()). CalcFoo adalah satu-satunya hal yang ingin saya ketahui. Refactoring adalah untuk fungsi pribadi. Saya sudah diberitahu Anda tidak boleh menguji unit fungsi pribadi. Jadi Anda kiri mencoba unit test CalcFoo (dengan banyak pencarian) atau fungsi pembuka Anda (mengubahnya menjadi publik) hanya agar mereka dapat diuji unit tetapi penelepon tidak boleh menggunakannya.
coding4fun
3
"refactoring harus menjadi perhatian utama Anda; pengujian datang setelah itu" - Saya sangat tidak setuju! Poin utama dari unit test adalah membuat refactoring menjadi lebih tidak berisiko.
JacquesB
@ coding4fun: apakah Anda yakin kode Anda dirancang dengan benar, dan sesuai dengan prinsip tanggung jawab tunggal ? Mungkin kelas Anda melakukan terlalu banyak dan harus dibagi menjadi beberapa, kelas yang lebih kecil?
Arseni Mourzenko
@ JacquesB: jika metode menggunakan 100+ pencarian (dan mungkin melakukan hal-hal lain juga), tidak ada cara Anda dapat menulis unit test untuk itu. Integrasi, sistem, dan tes fungsional — mungkin (yang pada gilirannya akan mengurangi risiko regresi saat refactoring monster).
Arseni Mourzenko
1
@ user2357112: kesalahan saya, saya pikir kode melakukan panggilan ke 100+ pencarian, yaitu ke 100+ tabel pencarian. Saya mengedit jawabannya. Terima kasih telah menunjukkan ini.
Arseni Mourzenko