Как мне провести модульное тестирование генератора кода?

Я знаю, что это сложный и открытый вопрос, но я решил бросить его на пол и посмотреть, есть ли у кого-нибудь интересные предложения.

Я разработал генератор кода, который переносит наш интерфейс python в наш код C++ (сгенерированный через SWIG) и генерирует код, необходимый для предоставления его как WebServices. Когда я разрабатывал этот код, я использовал TDD, но я обнаружил, что мои тесты чертовски хрупкие. Поскольку каждый тест, по сути, хотел проверить, что для заданного бита входного кода (который оказывается заголовком C++) я получу заданный бит выведенного кода, я написал небольшой движок, который считывает определения тестов из входных файлов XML и генерирует тест случаи из этих ожиданий.

Проблема в том, что я боюсь вообще вносить изменения в код. Это и тот факт, что сами модульные тесты являются a: сложными и b: хрупкими.

Итак, я пытаюсь придумать альтернативные подходы к этой проблеме, и мне кажется, что я, возможно, решаю ее неправильно. Может быть, мне нужно больше сосредоточиться на результате, IE: действительно ли генерируемый мной код выполняется и делает то, что я хочу, а не выглядит ли код так, как я хочу.

Есть ли у кого-нибудь опыт чего-то подобного, которым они хотели бы поделиться?

Ответов (8)

Решение

Я начал писать краткое изложение своего опыта работы с собственным генератором кода, затем вернулся и перечитал ваш вопрос и обнаружил, что вы уже затронули те же проблемы сами, сосредоточьтесь на результатах выполнения, а не на макете / внешнем виде кода.

Проблема в том, что это сложно проверить, сгенерированный код может не подходить для фактического запуска в среде системы модульного тестирования, и как вы кодируете ожидаемые результаты?

Я обнаружил, что вам нужно разбить генератор кода на более мелкие части и протестировать их. Модульное тестирование генератора полного кода больше похоже на интеграционное тестирование, чем на модульное тестирование, если вы спросите меня.

Да, результаты - это ЕДИНСТВЕННОЕ значение. Настоящая рутинная работа - это написать фреймворк, который позволит вашему сгенерированному коду работать независимо ... проводить там свое время.

Если вы работаете на * nux, вы можете отказаться от фреймворка unittest в пользу сценария bash или make-файла. в Windows вы можете подумать о создании приложения / функции оболочки, которая запускает генератор, а затем использует код (как другой процесс) и тестирует его.

Третий вариант - сгенерировать код, а затем создать на его основе приложение, которое не содержит ничего, кроме unittest. Опять же, вам понадобится сценарий оболочки или еще что-то, чтобы запускать его для каждого ввода. Что касается того, как кодировать ожидаемое поведение, мне приходит в голову, что это можно сделать почти так же, как и для кода C++, просто используя сгенерированный интерфейс, а не интерфейс C++.

Напомним, что «модульное тестирование» - это только один из видов тестирования. Вы должны иметь возможность модульного тестирования внутренних частей вашего генератора кода. На самом деле вы смотрите на тестирование системного уровня (также известное как регрессионное тестирование). Это не просто семантика ... есть разные образы мышления, подходы, ожидания и т. Д. Это, безусловно, больше работы, но вам, вероятно, нужно укусить пулю и настроить набор сквозных регрессионных тестов: исправленные файлы C++ -> SWIG интерфейсы -> модули python -> известный вывод. Вы действительно хотите сравнить известный ввод (фиксированный код C++) с ожидаемым выводом (тем, что выходит из окончательной программы Python). Непосредственная проверка результатов генератора кода будет похожа на поиск объектных файлов ...

Просто хочу отметить, что вы все еще можете достичь детального тестирования при проверке результатов: вы можете тестировать отдельные фрагменты кода, вкладывая их в некоторый код настройки и проверки:

int x = 0;
GENERATED_CODE
assert(x == 100);

Если у вас есть сгенерированный код, собранный из более мелких фрагментов, и эти фрагменты не меняются часто, вы можете выполнять больше условий и тестировать немного лучше и, надеюсь, избежать прерывания всех ваших тестов при изменении специфики одного фрагмента.

Модульное тестирование - это просто тестирование определенного модуля. Поэтому, если вы пишете спецификацию для класса A, идеально, если у класса A нет реальных конкретных версий классов B и C.

Хорошо, потом я заметил, что тег для этого вопроса включает C++ / Python, но принципы те же:

    public class A : InterfaceA 
    {   
      InterfaceB b;

      InterfaceC c;

      public A(InterfaceB b, InterfaceC c)   {
          this._b = b;
          this._c = c;   }

      public string SomeOperation(string input)   
      {
          return this._b.SomeOtherOperation(input) 
               + this._c.EvenAnotherOperation(input); 
      } 
    }

Поскольку указанная выше система A внедряет интерфейсы в системы B и C, вы можете проводить модульное тестирование только системы A, не имея реальных функциональных возможностей, выполняемых какой-либо другой системой. Это модульное тестирование.

Вот умный способ подхода к Системе от создания до завершения, с разными спецификациями When для каждой части поведения:

public class When_system_A_has_some_operation_called_with_valid_input : SystemASpecification
{
    private string _actualString;

    private string _expectedString;

    private string _input;

    private string _returnB;

    private string _returnC;

    [It]
    public void Should_return_the_expected_string()
    {
        _actualString.Should().Be.EqualTo(this._expectedString);
    }

    public override void GivenThat()
    {
        var randomGenerator = new RandomGenerator();
        this._input = randomGenerator.Generate<string>();
        this._returnB = randomGenerator.Generate<string>();
        this._returnC = randomGenerator.Generate<string>();

        Dep<InterfaceB>().Stub(b => b.SomeOtherOperation(_input))
                         .Return(this._returnB);
        Dep<InterfaceC>().Stub(c => c.EvenAnotherOperation(_input))
                         .Return(this._returnC);

        this._expectedString = this._returnB + this._returnC;
    }

    public override void WhenIRun()
    {
        this._actualString = Sut.SomeOperation(this._input);
    }
}

Итак, в заключение, один модуль / спецификация может иметь несколько вариантов поведения, и спецификация растет по мере разработки модуля / системы; и если ваша тестируемая система зависит от других конкретных систем в ней, будьте осторожны.

Я бы порекомендовал выяснить набор известных результатов ввода-вывода, таких как некоторые более простые случаи, которые у вас уже есть, и выполнить модульное тестирование созданного кода . Вполне возможно, что при изменении генератора точная создаваемая строка может немного отличаться ... но вас действительно волнует, интерпретируется ли она таким же образом. Таким образом, если вы протестируете результаты, как если бы вы протестировали этот код, если бы это была ваша функция, вы узнаете, преуспевает ли он так, как вы хотите.

По сути, вы действительно хотите знать, будет ли ваш генератор производить то, что вы ожидаете, без физического тестирования всех возможных комбинаций (также: невозможно). Убедившись, что ваш генератор работает в соответствии с вашими ожиданиями, вы можете быть уверены в том, что генератор будет успешным во все более сложных ситуациях.

Таким образом, вы также можете создать набор регрессионных тестов (модульных тестов, которые должны продолжать работать правильно). Это поможет вам убедиться, что изменения в вашем генераторе не нарушают другие формы кода. Когда вы сталкиваетесь с ошибкой, которую не обнаружили ваши модульные тесты, вы можете включить ее, чтобы предотвратить подобные поломки.

Я считаю, что вам нужно проверять то, что вы создаете, больше, чем то, как вы это создаете.

В моем случае программа генерирует множество типов кода (C#, HTML, SCSS, JS и т. Д.), Которые компилируются в веб-приложение. Лучший способ уменьшить количество ошибок регрессии в целом - это протестировать само веб-приложение, а не тестировать генератор.

Не поймите меня неправильно, все еще существуют модульные тесты, проверяющие часть кода генератора, но самым большим ударом по нашим затратам стали тесты пользовательского интерфейса для самого сгенерированного приложения.

Поскольку мы его генерируем, мы также создаем красивую абстракцию в JS, которую мы можем использовать для программного тестирования приложения. Мы следовали некоторым идеям, изложенным здесь: http://code.tutsplus.com/articles/maintainable-automated-ui-tests--net-35089

Самое замечательное в том, что он действительно тестирует вашу систему от начала до конца, от генерации кода до того, что вы на самом деле создаете. Если тест не прошел, его легко отследить до того места, где сломался генератор.

Это довольно мило.

Удачи!