大規模なルックアップテーブルによるユニットテスト?


8

私たちのシステムは、計算やその他のロジックの多くの重要な情報をルックアップタイプテーブルから取得できるように構成されています。例としては、すべての種類の異なるレート(金利や拠出率など)、日付(有効日など)、およびすべての種類のさまざまなその他の情報があります。

なぜ彼らはすべてをこのように構成することに決めたのですか?この情報の一部はかなり頻繁に変更されるためです。たとえば、料金の一部は毎年変動します。彼らは、コードの変更を最小限に抑えることを試みました。期待は、ルックアップテーブルが変更され、コードが機能することだけです(コードの変更はありません)。

残念ながら、それは単体テストを困難にするだろうと思います。一部のロジックでは、100以上の異なるルックアップが行われる場合があります。私は間違いなく私たちのレートを返すモック可能なオブジェクトを作ることができますが、かなりのセットアップになるでしょう。私はそれか、統合テストを使用する(そしてそのデータベースにアクセスする)必要があると思います。私は正しいですか、それとももっと良い方法がありますか?助言がありますか?

編集:
応答が遅れて申し訳ありませんが、私は同じことで他の多くのものをジャグリングしながら、すべてを浸そうとしました。また、実装を同時に実行したいと思いました。私はさまざまなパターンを試して、満足できる解決策を構築しました。満足できないビジターパターンを試してみました。結局、私はオニオンアーキテクチャを使用することになりました。結果に満足しましたか?ちょっと。それが何かだと思います。ルックアップテーブルを使用すると、はるかに困難になります。

以下は、毎年変化するレートのテスト用のセットアップコードの小さな例です(imはfakeiteasyを使用しています)。

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

これは、いつでも変更できるレートの設定コードです(新しい金利が導入されるまでには数年かかる場合があります)。

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

これが私のドメインオブジェクトの1つのコンストラクタです。

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

なぜ私はそれを好きではないのですか?既存のテストは機能しますが、将来新しいテストを追加する場合は、このセットアップコードを確認して、新しいレートが追加されていることを確認する必要があります。関数名の一部としてテーブル名を使用することで、それをできるだけ明確にしようとしましたが、それはそれが何であるかと思います:)

回答:


16

単体テストは引き続き作成できます。あなたの質問が説明しているのは、コードが依存するいくつかのデータソースがあるシナリオです。これらのデータソースは、すべてのテストにわたって同じ偽のデータを生成する必要があります。ただし、1つのテストごとに応答を設定することに関連する煩雑さは避けたいものです。必要なのはテスト偽物

テストフェイクは、アヒルのように見えたり、アヒルのように鳴ったりするものの実装ですが、テストの目的で一貫した応答を提供する以外は何もしません。


あなたの場合、あなたはIExchangeRateLookupインターフェースとそれの生産実装を持っているかもしれません

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

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

テスト中のコードのインターフェースに依存することにより、それを実装するあらゆるものを渡すことができます。

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

8

事実:

一部のロジックでは、100以上の異なるルックアップが行われる場合があります。

単体テストのコンテキストでは関係ありません。単体テストはコードのごく一部、通常はメソッドに焦点を当てており、単一のメソッドで100以上のルックアップテーブルが必要になることはほとんどありません(必要な場合は、リファクタリングが最大の懸念事項です。テストはその後に行われます)。同じテーブルへのループでの100回以上のルックアップを意味しない限り、その場合は問題ありません。

これらのルックアップにスタブとモックを追加することの複雑さは、単一の単体テストの規模でも気になりません。テスト内では、メソッドで実際に使用されているルックアップのルックアップのみをスタブ/モックします。それらがたくさんあるだけでなく、それらのスタブやモックも非常にシンプルになります。たとえば、メソッドが探しているものに関係なく、実際のルックアップに同じ数値が入力されているかのように、単一の値が返される場合があります。

複雑さが問題になるのは、ビジネスロジックをテストする必要があるときです。100以上のルックアップは、テストする数千および数千の異なるビジネスケース(外部ルックアップを含む)を意味します。つまり、数千および数千のユニットテストを意味します。

たとえば、OLAPキューブのコンテキストでは、2つのキューブに依存するメソッドがあり、1つは2次元、もう1つは5次元です。

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

そのままでは、メソッドを単体テストすることはできません。最初のステップは、OLAPキューブをスタブで置き換えることを可能にすることです。これを行う1つの方法は、依存性注入を使用することです。

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

これで、ユニットテストは次のようにスタブを挿入できます。

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

そのように使用されます:

var helloWorld = new HelloWorld(new OlapCubeStub(), new OlapCubeStub());
var actual = helloWorld.Demo();
var expected = 9;
this.AssertEquals(expected, actual);

返信いただきありがとうございます。リファクタリングは間違いなく賢いと思いますが、計算が非常に複雑な場合はどうしますか(CalcFoo()と呼びます)。CalcFooは、公開したい唯一のものです。リファクタリングはプライベート関数になります。私はプライベート関数をユニットテストしてはいけないと言われています。だから、あなたは左がCalcFoo(多くのルックアップで)やユニット化関数(それらをpublicに変更する)を単体テストしようとしています。
encoding4fun

3
「リファクタリングはあなたの最大の関心事であるべきです;テストはその後に来ます」-私は強く同意しません!単体テストの主なポイントは、リファクタリングのリスクを軽減することです。
JacquesB 2015年

@ coding4fun:コードが正しく設計されていて、それが単一責任の原則に準拠していることを確認しますか?多分あなたのクラスはあまりにも多くをやっていて、いくつかのより小さなクラスに分割されるべきですか?
Arseni Mourzenko

@JacquesB:メソッドが100以上のルックアップを使用している場合(そしておそらく他のことも実行している場合)、ユニットテストを作成する方法はありません。統合テスト、システムテスト、機能テスト(おそらく、モンスターをリファクタリングする際のリグレッションのリスクを軽減します)。
Arseni Mourzenko 2015年

1
@ user2357112:私の間違いは、コードが100以上のルックアップ、つまり100以上のルックアップテーブルを呼び出すと思っていました。回答を編集しました。これを指摘していただきありがとうございます。
Arseni Mourzenko 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.