Teste de unidade com tabelas de pesquisa massivas?

8

Nosso sistema está estruturado de tal maneira que obtemos muitas informações importantes para nossos cálculos e outras lógicas dessas nas tabelas de tipos de pesquisa. Os exemplos seriam todos os tipos de taxas diferentes (como taxas de juros ou taxas de contribuição), datas (como datas efetivas) e todos os tipos de informações diversas.

Por que eles decidiram estruturar tudo assim? Porque algumas dessas informações mudam com bastante frequência. Por exemplo, algumas de nossas tarifas mudam anualmente. Eles queriam tentar minimizar as alterações de código. A esperança era apenas que as tabelas de pesquisa mudassem e o código funcionasse (sem alterações de código).

Infelizmente, acho que vai tornar o teste de unidade desafiador. Parte da lógica pode fazer mais de 100 pesquisas diferentes. Embora eu possa definitivamente criar um objeto zombeteiro que retorne nossas taxas, haverá uma configuração considerável. Eu acho que é isso ou eu tenho que acabar usando testes de integração (e atingindo esse banco de dados). Estou certo ou existe uma maneira melhor? Alguma sugestão?

Edit:
Desculpe pela resposta atrasada, mas eu estava tentando absorver tudo enquanto, ao mesmo tempo, malabarismo com muitas outras coisas. Eu também queria tentar trabalhar com a implementação e ao mesmo tempo. Tentei uma variedade de padrões para tentar arquitetar a solução para algo que me agradava. Eu tentei o padrão de visitantes que eu não estava feliz. No final, acabei usando a arquitetura da cebola. Fiquei feliz com os resultados? Tipo de. Eu acho que é o que é. As tabelas de pesquisa tornam muito mais desafiador.

Aqui está um pequeno exemplo (estou usando fakeiteasy) de código de configuração para os testes para uma taxa que muda 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);
}

Aqui está um código de configuração para uma taxa que pode mudar a qualquer momento (pode levar anos até que uma nova taxa de juros seja introduzida):

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

Aqui está o construtor de um dos meus objetos de domínio:

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

Então, por que eu não gosto? Os testes existentes funcionarão, mas qualquer pessoa que adicionar um novo teste no futuro precisará examinar esse código de instalação para garantir que novas taxas sejam adicionadas. Eu tentei deixar o mais claro possível usando o nome da tabela como parte do nome da função, mas acho que é o que é :)

coding4fun
fonte

Respostas:

16

Você ainda pode escrever testes de unidade. O que sua pergunta descreve é ​​um cenário em que você tem algumas fontes de dados das quais seu código depende. Essas fontes de dados precisam produzir os mesmos dados falsos em todos os seus testes. No entanto, você não deseja a confusão associada à configuração de respostas para cada teste. O que você precisa são testes falsos

Um teste falso é uma implementação de algo que se parece com um pato e grasna como um pato, mas não faz nada além de fornecer respostas consistentes para fins de teste.


No seu caso, você pode ter uma IExchangeRateLookupinterface e uma implementação de produção

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

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

Dependendo da interface do código em teste, você pode transmitir qualquer coisa que a implemente, incluindo um 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
fonte
8

O fato de que:

Parte da lógica pode fazer mais de 100 pesquisas diferentes.

é irrelevante em um contexto de teste de unidade. Os testes de unidade concentram-se em uma pequena parte do código, geralmente um método, e é improvável que um único método precise de mais de 100 tabelas de pesquisa (se houver, a refatoração deve ser sua principal preocupação; o teste vem depois disso). A menos que você queira dizer mais de 100 pesquisas em um loop na mesma tabela, nesse caso, você está bem.

A complexidade de adicionar stubs e zombarias para essas pesquisas não deve incomodá-lo na escala de um único teste de unidade. Dentro do teste, você irá stub / mock apenas as pesquisas que estão realmente em uso pelo método. Não apenas você não terá muitos deles, mas também esses stubs ou zombarias serão muito simples. Por exemplo, eles podem retornar um único valor, não importa o que o método esteja procurando (como se uma pesquisa real fosse preenchida com o mesmo número).

Quando a complexidade importa, é quando você terá que testar a lógica de negócios. Mais de 100 pesquisas provavelmente significam milhares e milhares de casos de negócios diferentes para testar (mesmo pesquisas externas), o que significa milhares e milhares de testes de unidade.

Ilustração

Por exemplo, em um contexto de um cubo OLAP, você pode ter um método que se baseia em dois cubos, um com duas dimensões e outro com cinco dimensões:

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 é, o método não pode ser testado em unidade. A primeira etapa é possibilitar a substituição de cubos OLAP por stubs. Uma maneira de fazer isso é através da injeção de dependência.

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

Agora, um teste de unidade pode injetar um esboço como este:

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

e usado assim:

var helloWorld = new HelloWorld(new OlapCubeStub(), new OlapCubeStub());
var actual = helloWorld.Demo();
var expected = 9;
this.AssertEquals(expected, actual);
Arseni Mourzenko
fonte
Obrigado pela resposta. Enquanto eu acho que a refatoração é definitivamente inteligente, o que você faz no caso em que o cálculo é muito complexo (chame isso de CalcFoo ()). CalcFoo é a única coisa que quero que seja exposta. A refatoração seria para funções privadas. Foi-me dito que você nunca deve testar funções particulares. Portanto, sua esquerda está tentando testar o CalcFoo (com muitas pesquisas) ou as funções de abertura (alterando-as para públicas) apenas para que possam ser testadas em unidade, mas o chamador nunca deve usá-las.
Coding4Fun
3
"a refatoração deve ser sua principal preocupação; os testes vêm depois disso" - discordo totalmente! Um ponto importante dos testes de unidade é tornar a refatoração menos arriscada.
JacquesB
@ coding4fun: você tem certeza de que seu código está arquitetado corretamente e está em conformidade com o princípio de responsabilidade única ? Talvez a sua turma esteja fazendo muito e deva ser dividida em várias turmas menores?
Arseni Mourzenko
@ JacquesB: se um método usa mais de 100 pesquisas (e provavelmente faz outras coisas também), não há como você escrever testes de unidade para ele. Testes de integração, sistema e funcionais - talvez (o que, por sua vez, reduzirá o risco de regressões ao refatorar o monstro).
Arseni Mourzenko
1
@ user2357112: meu erro, pensei que o código estava fazendo chamadas para mais de 100 pesquisas, ou seja, para mais de 100 tabelas de pesquisa. Eu editei a resposta. Obrigado por apontar isto.
Arseni Mourzenko