Unit Testing mit massiven Nachschlagetabellen?

8

Unser System ist so strukturiert, dass wir viele wichtige Informationen für unsere Berechnungen und andere solche Logik aus Nachschlagetabellen erhalten. Beispiele wären alle Arten von unterschiedlichen Sätzen (wie Zinssätze oder Beitragssätze), Daten (wie Datum des Inkrafttretens) und alle Arten von verschiedenen sonstigen Informationen.

Warum haben sie beschlossen, alles so zu strukturieren? Weil sich einige dieser Informationen ziemlich oft ändern. Zum Beispiel ändern sich einige unserer Preise jährlich. Sie wollten versuchen, Codeänderungen zu minimieren. Die Hoffnung war nur, dass sich die Nachschlagetabellen ändern würden und der Code einfach funktionieren würde (keine Codeänderungen).

Leider denke ich, dass es Unit-Tests schwierig machen wird. Einige der Logikfunktionen können mehr als 100 verschiedene Suchvorgänge ausführen. Während ich definitiv ein verspottbares Objekt machen kann, das unsere Preise zurückgibt, wird es ein beträchtliches Setup geben. Ich denke, es ist entweder das oder ich muss am Ende Integrationstests verwenden (und diese Datenbank treffen). Habe ich recht oder gibt es einen besseren Weg? Irgendwelche Vorschläge?

Bearbeiten:
Entschuldigung für die verspätete Antwort, aber ich habe versucht, alles in mich aufzunehmen, während ich gleichzeitig mit vielen anderen Dingen jonglierte. Ich wollte auch versuchen, die Implementierung durchzuarbeiten und gleichzeitig. Ich habe verschiedene Muster ausprobiert, um die Lösung für etwas zu finden, mit dem ich zufrieden war. Ich habe das Besuchermuster ausprobiert, mit dem ich nicht zufrieden war. Am Ende habe ich die Zwiebelarchitektur verwendet. War ich mit den Ergebnissen zufrieden? Art von. Ich denke es ist was es ist. Die Nachschlagetabellen machen es viel schwieriger.

Hier ist ein kleines Beispiel (ich verwende fakeiteasy) des Setup-Codes für die Tests für eine Rate, die sich jährlich ändert:

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

Hier ist ein Setup-Code für einen Zinssatz, der sich jederzeit ändern kann (es kann Jahre dauern, bis ein neuer Zinssatz eingeführt wird):

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

Hier ist der Konstruktor für eines meiner Domänenobjekte:

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

Warum mag ich es nicht? Gut vorhandene Tests werden funktionieren, aber jeder, der in Zukunft neue Tests hinzufügt, muss diesen Setup-Code durchsehen, um sicherzustellen, dass neue Raten hinzugefügt werden. Ich habe versucht, es so klar wie möglich zu machen, indem ich den Tabellennamen als Teil des Funktionsnamens verwendet habe, aber ich denke, es ist das, was es ist :)

Codierung4fun
quelle

Antworten:

16

Sie können weiterhin Komponententests schreiben. In Ihrer Frage wird ein Szenario beschrieben, in dem Sie über einige Datenquellen verfügen, von denen Ihr Code abhängt. Diese Datenquellen müssen bei allen Tests dieselben gefälschten Daten erzeugen . Sie möchten jedoch nicht, dass die Unordnung mit dem Einrichten von Antworten für jeden einzelnen Test verbunden ist. Was Sie brauchen, sind Testfälschungen

Eine Testfälschung ist eine Implementierung von etwas, das wie eine Ente aussieht und wie eine Ente quakt, aber nichts anderes tut, als zu Testzwecken konsistente Antworten zu geben.


In Ihrem Fall verfügen Sie möglicherweise über eine IExchangeRateLookupSchnittstelle und eine Produktionsimplementierung

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

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

Abhängig von der Schnittstelle im zu testenden Code können Sie alles übergeben, was es implementiert, einschließlich einer Fälschung

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
quelle
8

Die Tatsache, dass:

Einige der Logikfunktionen können mehr als 100 verschiedene Suchvorgänge ausführen.

ist im Zusammenhang mit Unit-Tests irrelevant. Ein Komponententest konzentriert sich auf einen kleinen Teil des Codes, normalerweise eine Methode, und es ist unwahrscheinlich, dass eine einzelne Methode mehr als 100 Nachschlagetabellen benötigt (wenn dies der Fall ist, sollte das Refactoring Ihr Hauptanliegen sein; das Testen erfolgt danach). Wenn Sie nicht mehr als 100 Suchvorgänge in einer Schleife für dieselbe Tabelle meinen, ist dies in diesem Fall in Ordnung.

Die Komplexität des Hinzufügens von Stubs und Mocks für diese Suchvorgänge sollte Sie auch bei einem einzelnen Einheitentest nicht stören. Innerhalb des Tests werden Sie nur diejenigen der Lookups stub / verspotten, die tatsächlich von der Methode verwendet werden. Sie werden nicht nur nicht viele davon haben, sondern auch diese Stubs oder Mocks werden sehr einfach sein. Beispielsweise können sie einen einzelnen Wert zurückgeben, unabhängig davon, wonach die Methode sucht (als ob eine tatsächliche Suche mit derselben Nummer gefüllt wäre).

Wenn die Komplexität eine Rolle spielt, müssen Sie die Geschäftslogik testen. Über 100 Suchvorgänge bedeuten wahrscheinlich Tausende und Abertausende verschiedener zu testender Geschäftsfälle (auch außerhalb von Suchvorgängen), dh Tausende und Abertausende von Komponententests.

Illustration

In einem Kontext eines OLAP-Cubes verfügen Sie möglicherweise über eine Methode, die auf zwei Cubes basiert, einen mit zwei Dimensionen und einen mit fünf Dimensionen:

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

Die Methode kann nicht auf Einheit getestet werden. Der erste Schritt besteht darin, OLAP-Cubes durch Stubs zu ersetzen. Eine Möglichkeit hierfür ist die Abhängigkeitsinjektion.

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

Jetzt kann ein Unit-Test einen Stub wie folgt injizieren:

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

und so verwendet:

var helloWorld = new HelloWorld(new OlapCubeStub(), new OlapCubeStub());
var actual = helloWorld.Demo();
var expected = 9;
this.AssertEquals(expected, actual);
Arseni Mourzenko
quelle
Danke für die Antwort. Während ich denke, Refactoring ist definitiv klug, was machen Sie, wenn Sie eine Berechnung haben, die sehr komplex ist (nennen Sie es CalcFoo ()). CalcFoo ist das einzige, was ich ausgesetzt sein möchte. Das Refactoring würde sich auf private Funktionen beziehen. Mir wurde gesagt, Sie sollten niemals private Funktionen testen. Ihre Linke versucht also, CalcFoo (mit vielen Suchvorgängen) einem Unit-Test zu unterziehen oder Funktionen zu öffnen (sie in öffentlich zu ändern), damit sie Unit-getestet werden können, aber der Anrufer sollte sie niemals verwenden.
Codierung4fun
3
"Refactoring sollte Ihr Hauptanliegen sein; Tests folgen danach" - ich bin absolut anderer Meinung! Ein wichtiger Punkt bei Unit-Tests ist es, das Refactoring weniger riskant zu machen.
JacquesB
@oding4fun: Sind Sie sicher, dass Ihr Code korrekt aufgebaut ist und dem Prinzip der Einzelverantwortung entspricht ? Vielleicht macht Ihre Klasse zu viel und sollte in mehrere kleinere Klassen aufgeteilt werden?
Arseni Mourzenko
@JacquesB: Wenn eine Methode mehr als 100 Lookups verwendet (und wahrscheinlich auch andere Dinge), können Sie keine Komponententests dafür schreiben. Integrations-, System- und Funktionstests - möglicherweise (was wiederum das Risiko von Regressionen bei der Umgestaltung des Monsters verringert).
Arseni Mourzenko
1
@ user2357112: Mein Fehler, ich dachte, der Code ruft mehr als 100 Lookups auf, dh mehr als 100 Lookup-Tabellen. Ich habe die Antwort bearbeitet. Vielen Dank für den Hinweis.
Arseni Mourzenko