Test unitario con enormi tabelle di ricerca?

8

Il nostro sistema è strutturato in modo tale da ottenere molte informazioni chiave per i nostri calcoli e altre logiche simili dalle tabelle dei tipi di ricerca. Gli esempi potrebbero essere tutti i tipi di tassi diversi (come i tassi di interesse o tassi di contributo), le date (come le date effettive) e tutti i tipi di informazioni misc varie.

Perché hanno deciso di strutturare tutto come questo? Perché alcune di queste informazioni cambiano abbastanza spesso. Ad esempio alcune delle nostre tariffe cambiano ogni anno. Volevano cercare di minimizzare le modifiche al codice. La speranza era solo che le tabelle di ricerca cambiassero e il codice avrebbe funzionato (nessuna modifica al codice).

Sfortunatamente, penso che renderà i test unitari difficili. Alcune delle logiche potrebbero rendere più di 100 ricerche diverse. Mentre posso sicuramente creare un oggetto irrisorio che restituisce le nostre tariffe, ci sarà un setup considerevole. Penso che sia quello che devo finire per usare i test di integrazione (e colpire quel database). Ho ragione o c'è un modo migliore? Qualche suggerimento?

Modifica:
Ci scusiamo per la risposta ritardata, ma stavo cercando di assorbire tutto mentre ho la stessa cosa a destreggiarsi tra molte altre cose. Volevo anche provare a lavorare attraverso l'implementazione e allo stesso tempo. Ho provato una varietà di modelli per provare ad architettare la soluzione a qualcosa di cui ero felice. Ho provato il modello di visitatore di cui non ero contento. Alla fine ho finito per usare l'architettura della cipolla. Ero felice con i risultati? Una specie di. Penso che sia quello che è. Le tabelle di ricerca lo rendono molto più impegnativo.

Ecco un piccolo esempio (im using fakeiteasy) del codice di installazione per i test per un tasso che cambia annualmente:

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);
}

Ecco alcuni codici di configurazione per un tasso che può cambiare in qualsiasi momento (potrebbero passare anni prima che venga introdotto un nuovo tasso di interesse):

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);
}

Ecco il costruttore di uno dei miei oggetti di dominio:

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;
    }

Quindi perché non mi piace? I test già esistenti funzioneranno, ma chiunque aggiungerà nuovi test in futuro dovrà esaminare questo codice di configurazione per assicurarsi che vengano aggiunte nuove tariffe. Ho cercato di renderlo il più chiaro possibile usando il nome della tabella come parte del nome della funzione, ma suppongo che sia quello che è:)

    
posta coding4fun 24.06.2015 - 15:06
fonte

2 risposte

16

È ancora possibile scrivere test unitari. Quello che la tua domanda descrive è uno scenario in cui hai alcune fonti di dati da cui dipende il tuo codice. Queste origini dati devono produrre gli stessi dati falsi in tutti i tuoi test. Tuttavia, non si desidera la confusione associata alla configurazione delle risposte per ogni singolo test. Ciò di cui hai bisogno sono falsi test

Un falso di prova è un'implementazione di qualcosa che assomiglia ad un'anatra e ciarlona come un'anatra, ma non fa nulla se non fornire risposte coerenti ai fini del test.

Nel tuo caso, potresti avere un'interfaccia IExchangeRateLookup e un'implementazione di produzione di essa

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

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

In base all'interfaccia nel codice sottoposto a test, puoi passare tutto ciò che lo implementa, incluso un falso

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];
    }
}
    
risposta data 24.06.2015 - 15:17
fonte
8

Il fatto che:

Some of the logic might make 100+ different lookups.

è irrilevante in un contesto di test unitario. Un test unitario si concentra su una piccola parte del codice, di solito un metodo, ed è improbabile che un singolo metodo abbia bisogno di oltre 100 tabelle di ricerca (se lo fa, il refactoring dovrebbe essere la vostra principale preoccupazione, il testing viene dopo). A meno che tu non intenda più di 100 ricerche in un ciclo per la stessa tabella, nel qual caso, stai bene.

La complessità di aggiungere stub e mock per quelle ricerche non dovrebbe infastidirti né su una scala di un singolo test di unità. All'interno del test, si stub / mock solo quelli delle ricerche che sono effettivamente in uso con il metodo. Non solo non ne avrai molti, ma anche quei mozziconi o moccietti saranno molto semplici. Ad esempio, possono restituire un singolo valore, indipendentemente dal metodo cercato (come se una ricerca effettiva fosse riempita con lo stesso numero).

Quando la complessità conta è quando dovrai testare la logica di business. 100+ ricerche probabilmente significano migliaia e migliaia di casi aziendali diversi da testare (anche ricerche esterne), il che significa migliaia e migliaia di test unitari.

Illustrazione

Ad esempio, in un contesto di un cubo OLAP, potresti avere un metodo che si basa su due cubi, uno con due dimensioni e uno con cinque dimensioni:

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);
        ...
    }
}

Il metodo non può essere testato unitamente. Il primo passo è rendere possibile la sostituzione dei cubi OLAP con stub. Un modo per farlo è attraverso Dipendenza Iniezione.

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);
        ...
    }
}

Ora un test unitario può iniettare uno stub come questo:

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

e usato in questo modo:

var helloWorld = new HelloWorld(new OlapCubeStub(), new OlapCubeStub());
var actual = helloWorld.Demo();
var expected = 9;
this.AssertEquals(expected, actual);
    
risposta data 24.06.2015 - 15:14
fonte

Leggi altre domande sui tag