¿Pruebas unitarias con tablas de búsqueda masivas?

8

Nuestro sistema está estructurado de tal manera que obtenemos mucha información clave para nuestros cálculos y otra lógica de las tablas de tipos de búsqueda. Los ejemplos serían todo tipo de tasas diferentes (como tasas de interés o tasas de contribución), fechas (como fechas de vigencia) y todo tipo de información variada.

¿Por qué decidieron estructurar todo de esta manera? Porque parte de esta información cambia con bastante frecuencia. Por ejemplo, algunas de nuestras tarifas cambian anualmente. Querían tratar de minimizar los cambios de código. La esperanza era que las tablas de búsqueda cambiarían y el código simplemente funcionaría (sin cambios de código).

Desafortunadamente, creo que hará que las pruebas unitarias sean un desafío. Parte de la lógica podría hacer más de 100 búsquedas diferentes. Si bien definitivamente puedo hacer un objeto simulable que devuelva nuestras tarifas, habrá una configuración considerable. Creo que es eso o tengo que terminar usando pruebas de integración (y golpeando esa base de datos). ¿Estoy en lo cierto o hay una mejor manera? ¿Alguna sugerencia?

Editar:
Perdón por la respuesta tardía, pero estaba tratando de empapar todo mientras al mismo tiempo hacía malabares con muchas otras cosas. También quería intentar trabajar en la implementación y al mismo tiempo. Intenté una variedad de patrones para tratar de diseñar la solución a algo con lo que estaba contento. Probé el patrón de visitante con el que no estaba contento. Al final terminé usando la arquitectura de cebolla. ¿Estaba contento con los resultados? Algo así como. Creo que es lo que es. Las tablas de búsqueda lo hacen mucho más desafiante.

Aquí hay un pequeño ejemplo (estoy usando fakeiteasy) de código de configuración para las pruebas para una tasa que cambia anualmente:

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

Aquí hay un código de configuración para una tasa que puede cambiar en cualquier momento (pueden pasar años antes de que se introduzca una nueva tasa de interés):

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

Aquí está el constructor de uno de mis objetos de 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;
    }

Entonces, ¿por qué no me gusta? Las pruebas existentes funcionarán, pero cualquiera que agregue una nueva prueba en el futuro tendrá que revisar este código de configuración para asegurarse de que se agreguen nuevas tasas. Traté de hacerlo lo más claro posible usando el nombre de la tabla como parte del nombre de la función, pero supongo que es lo que es :)

coding4fun
fuente

Respuestas:

16

Todavía puedes escribir pruebas unitarias. Lo que describe su pregunta es un escenario en el que tiene algunas fuentes de datos de las que depende su código. Estas fuentes de datos deben producir los mismos datos falsos en todas sus pruebas. Sin embargo, no desea el desorden asociado con la configuración de respuestas para cada prueba. Lo que necesitas son falsificaciones de prueba

Una prueba falsa es una implementación de algo que parece un pato y grazna como un pato, pero no hace nada más que proporcionar respuestas consistentes a los fines de la prueba.


En su caso, puede tener una IExchangeRateLookupinterfaz y una implementación de producción.

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

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

Al depender de la interfaz en el código que se está probando, puede pasar cualquier cosa que lo implemente, incluido 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];
    }
}
Andy Hunt
fuente
8

El hecho de que:

Parte de la lógica podría hacer más de 100 búsquedas diferentes.

es irrelevante en un contexto de pruebas unitarias. Las pruebas unitarias se centran en una pequeña parte del código, generalmente un método, y es poco probable que un solo método necesite más de 100 tablas de búsqueda (si es necesario, la refactorización debería ser su principal preocupación; las pruebas se realizarán después de eso). A menos que se refiera a más de 100 búsquedas en un bucle a la misma tabla, en cuyo caso, está bien.

La complejidad de agregar talones y simulacros para esas búsquedas tampoco debería molestarlo a escala de una sola prueba de unidad. Dentro de la prueba, se restregará / simulará solo aquellos de las búsquedas que el método realmente esté utilizando. No solo no tendrás muchos de ellos, sino que también esos trozos o simulacros serán muy simples. Por ejemplo, pueden devolver un solo valor, sin importar lo que busque el método (como si una búsqueda real se completara con el mismo número).

Cuando la complejidad importará es cuando tendrá que probar la lógica empresarial. Más de 100 búsquedas probablemente significan miles y miles de casos comerciales diferentes para probar (incluso búsquedas externas), lo que significa miles y miles de pruebas unitarias.

Ilustración

Por ejemplo, en el contexto de un cubo OLAP, puede tener un método que se base en dos cubos, uno con dos dimensiones y otro con cinco dimensiones:

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

Como es, el método no puede ser probado por la unidad. El primer paso es hacer posible reemplazar cubos OLAP por apéndices. Una forma de hacerlo es a través de la inyección de dependencia.

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

Ahora una prueba unitaria puede inyectar un trozo como este:

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

y usado así:

var helloWorld = new HelloWorld(new OlapCubeStub(), new OlapCubeStub());
var actual = helloWorld.Demo();
var expected = 9;
this.AssertEquals(expected, actual);
Arseni Mourzenko
fuente
Gracias por la respuesta. Si bien creo que la refactorización es definitivamente inteligente, ¿qué hacer en el caso de que tenga un cálculo es muy complejo (llámelo CalcFoo ()). CalcFoo es lo único que quiero exponer. La refactorización sería a funciones privadas. Me han dicho que nunca debe probar las funciones privadas de la unidad. Entonces, su izquierda tratando de probar la unidad CalcFoo (con muchas búsquedas) o sus funciones de apertura (cambiándolas a públicas) solo para que puedan probarse en la unidad, pero la persona que llama nunca debe usarlas.
coding4fun
3
"la refactorización debería ser su principal preocupación; las pruebas vienen después de eso" - ¡Estoy totalmente en desacuerdo! Un punto importante de las pruebas unitarias es hacer que la refactorización sea menos riesgosa.
JacquesB
@ coding4fun: ¿está seguro de que su código está diseñado correctamente y que cumple con el principio de responsabilidad única ? ¿Quizás tu clase está haciendo demasiado y debería dividirse en varias clases más pequeñas?
Arseni Mourzenko
@JacquesB: si un método usa más de 100 búsquedas (y probablemente también hace otras cosas), no hay forma de que pueda escribir pruebas unitarias para él. Integración, sistema y pruebas funcionales, tal vez (lo que a su vez reducirá el riesgo de regresiones al refactorizar el monstruo).
Arseni Mourzenko
1
@ user2357112: mi error, pensé que el código estaba haciendo llamadas a más de 100 búsquedas, es decir, a más de 100 tablas de búsqueda. Edité la respuesta. Gracias por señalar esto.
Arseni Mourzenko